-parameter generics for transformation
function fuseConfigurations<TBase, TOverride>(
base: TBase,
override: TOverride
): TBase & TOverride {
return { ...base, ...override };
}
const merged = fuseConfigurations(
{ host: 'localhost', port: 3000 },
{ port: 8080, debug: true }
);
// Type: { host: string; port: number } & { port: number; debug: boolean }
// Result: { host: 'localhost', port: 8080, debug: true }
**Architecture Decision:** Use generic functions for pure transformations and utilities. Rely on inference to reduce boilerplate; explicit type arguments should only be used when inference fails or to enforce specific constraints.
#### 2. Generic Interfaces for API Contracts
Generic interfaces define reusable shapes for data structures, particularly useful for API responses and service envelopes.
```typescript
interface ServiceEnvelope<TData, TMeta = Record<string, unknown>> {
status: 'ok' | 'error';
data: TData;
meta: TMeta;
timestamp: string;
}
// Specialized types via generic application
type UserLookupResponse = ServiceEnvelope<{ id: string; displayName: string }>;
type PaginatedList<TItem> = ServiceEnvelope<TItem[], { page: number; limit: number; total: number }>;
const userResponse: UserLookupResponse = {
status: 'ok',
data: { id: 'usr_123', displayName: 'Alice' },
meta: { region: 'us-east' },
timestamp: '2026-05-16T10:00:00Z',
};
Rationale: Separating the envelope structure from the payload type allows consistent handling of metadata and status across all API calls while maintaining strict typing for the payload.
3. Generic Classes for State Management
Generic classes enable type-safe collections and state containers. Constraints ensure that stored items meet specific requirements.
class InMemoryCache<TItem extends { id: string }> {
private store = new Map<string, TItem>();
set(item: TItem): void {
this.store.set(item.id, item);
}
get(id: string): TItem | undefined {
return this.store.get(id);
}
getAll(): TItem[] {
return Array.from(this.store.values());
}
remove(id: string): boolean {
return this.store.delete(id);
}
}
// Usage with specific entity shapes
interface Product { id: string; sku: string; price: number; }
interface Cart { id: string; items: string[]; }
const productCache = new InMemoryCache<Product>();
productCache.set({ id: 'prod_1', sku: 'A-100', price: 29.99 });
const cartCache = new InMemoryCache<Cart>();
cartCache.set({ id: 'cart_1', items: ['prod_1'] });
Decision: Apply generics to classes when the class manages a collection or state of a homogeneous type. Use constraints to enforce structural requirements, such as the presence of an identifier.
4. Constraints and Dynamic Property Access
Constraints restrict generic types to specific shapes, enabling safe access to properties. The keyof operator allows dynamic property access with full type safety.
// Retrieves an item by a specific attribute key
function retrieveByAttribute<T, K extends keyof T>(
collection: T[],
key: K,
value: T[K]
): T | undefined {
return collection.find(item => item[key] === value);
}
interface Employee { id: number; department: string; level: number; }
const staff: Employee[] = [
{ id: 1, department: 'Engineering', level: 3 },
{ id: 2, department: 'Design', level: 2 },
];
const engineer = retrieveByAttribute(staff, 'department', 'Engineering');
// Type of engineer: Employee | undefined
// Invalid usage caught at compile time:
// retrieveByAttribute(staff, 'salary', 50000); // Error: 'salary' not in keyof Employee
Rationale: keyof constraints prevent runtime errors caused by accessing non-existent properties. This pattern is essential for building flexible query layers and data mappers.
5. Conditional Types and Inference
Conditional types enable type-level logic, allowing types to transform based on input shapes. The infer keyword extracts types from complex structures.
// Extracts the payload type from a ServiceEnvelope
type ExtractPayload<T> = T extends ServiceEnvelope<infer P> ? P : never;
type UserPayload = ExtractPayload<UserLookupResponse>;
// Type: { id: string; displayName: string }
// Non-nullable wrapper
type SafeValue<T> = T extends null | undefined ? never : T;
type StrictString = SafeValue<string | null>;
// Type: string
Architecture Decision: Use conditional types for type transformations and utility libraries. They are powerful for building type-safe wrappers and extracting nested types without manual annotation.
Pitfall Guide
-
The any Leakage
- Explanation: Using
any inside a generic function breaks type safety, allowing invalid data to pass through.
- Fix: Ensure all internal logic respects the generic type
T. Use constraints or type guards instead of any.
-
Missing Constraints
- Explanation: Assuming properties exist on
T without constraints leads to compile errors or unsafe access.
- Fix: Always apply
extends constraints when accessing properties. E.g., T extends { id: string }.
-
Over-Generalization
- Explanation: Making every function generic when specific types suffice adds unnecessary complexity.
- Fix: Apply generics only where variability exists. If a function only handles one shape, use concrete types.
-
Complexity Spiral
- Explanation: Deeply nested generics or complex conditional types can make error messages unreadable and debugging difficult.
- Fix: Break down complex types into aliases. Use descriptive names. Avoid excessive nesting; prefer composition.
-
Ignoring Variance
- Explanation: Misunderstanding covariance and contravariance can lead to assignability errors, especially with function parameters.
- Fix: Understand that function parameters are contravariant. Use
readonly modifiers for covariant positions. Test assignability explicitly.
-
Utility Type Misuse
- Explanation: Using
Partial when Pick is needed, or vice versa, results in incorrect shapes.
- Fix: Choose utility types based on intent:
Pick for selection, Omit for exclusion, Partial for optionality, Required for strictness.
-
Runtime vs. Compile Time Confusion
- Explanation: Generics are erased at runtime. Using
T in runtime checks (e.g., instanceof T) fails.
- Fix: Never use generic type parameters in runtime code. Use value-based checks or pass constructor functions if runtime type info is needed.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| API Client Response | Generic Function | Type-safe payload extraction | Low |
| Event Dispatcher | Generic Class | Decoupled handler types | Medium |
| Config Merging | Utility Types | Shape transformation | Low |
| Dynamic Query | keyof Constraint | Safe property access | Low |
| Type Transformation | Conditional Types | Advanced type logic | Medium |
Configuration Template
A production-ready generic API client template with error handling and type safety.
interface ApiError { code: string; message: string; }
type ApiResponse<TData> =
| { status: 200; data: TData }
| { status: number; error: ApiError };
async function invokeEndpoint<TResponse>(
path: string,
options?: RequestInit
): Promise<ApiResponse<TResponse>> {
try {
const response = await fetch(path, {
headers: { 'Content-Type': 'application/json' },
...options,
});
if (!response.ok) {
const errorBody = await response.json().catch(() => ({}));
return {
status: response.status,
error: {
code: `HTTP_${response.status}`,
message: errorBody.message || 'Request failed',
},
};
}
const data = await response.json();
return { status: 200, data: data as TResponse };
} catch (err) {
return {
status: 0,
error: { code: 'NETWORK_ERROR', message: 'Fetch failed' },
};
}
}
// Usage
type User = { id: string; name: string };
const result = await invokeEndpoint<User>('/api/users/1');
if (result.status === 200) {
console.log(result.data.name); // Type-safe access
} else {
console.error(result.error.message);
}
Quick Start Guide
- Define the Shape: Identify the variable part of your data structure. Create a generic parameter
T to represent it.
- Create the Contract: Write a generic function or interface using
T. Apply constraints if properties are accessed.
- Verify Inference: Call the function with concrete arguments. Check that TypeScript infers the correct types without explicit annotations.
- Add Constraints: If the generic accesses properties, add
extends constraints to ensure structural compatibility.
- Integrate: Replace
any or union types in existing code with the new generic implementation. Run type checks to validate safety.