Generics for Reusable Patterns
Generics allow you to write flexible, reusable code while maintaining type safety. They're essential for implementing patterns that work with any type.
The Problem: Losing Type Information
Without generics, you lose type safety when building reusable components:
typescript1// Using 'any' loses type information2function firstElement(arr: any[]): any {3 return arr[0];4}56const packages = [{ weight: 5 }, { weight: 10 }];7const first = firstElement(packages);8// first is 'any' - no autocomplete, no type checking9console.log(first.wieght); // Typo not caught!
Generic Functions
Generics preserve type information:
typescript1function firstElement<T>(arr: T[]): T | undefined {2 return arr[0];3}45// Type is inferred6const packages = [{ weight: 5 }, { weight: 10 }];7const first = firstElement(packages);8// first is { weight: number } | undefined9console.log(first?.weight); // Autocomplete works!
The <T> declares a type parameter that captures the actual type used.
Multiple Type Parameters
Functions can have multiple type parameters:
typescript1function createPair<K, V>(key: K, value: V): [K, V] {2 return [key, value];3}45// Types inferred from arguments6const pair = createPair('trackingId', 'TRK123456');7// pair is [string, string]89const entry = createPair(1, { status: 'delivered' });10// entry is [number, { status: string }]
Generic Interfaces
Interfaces can be generic too:
typescript1// Generic container2interface Container<T> {3 contents: T;4 addItem(item: T): void;5 getItem(): T;6}78// Generic result type9interface Result<T, E = Error> {10 success: boolean;11 data?: T;12 error?: E;13}1415// Usage16interface Package {17 id: string;18 weight: number;19}2021const packageContainer: Container<Package> = {22 contents: { id: 'pkg_1', weight: 5 },23 addItem(item) { this.contents = item; },24 getItem() { return this.contents; }25};
Generic Constraints
Use extends to constrain what types are allowed:
typescript1// T must have a 'weight' property2interface Weighable {3 weight: number;4}56function calculateShipping<T extends Weighable>(item: T): number {7 return item.weight * 2.5; // Safe: weight is guaranteed8}910// Works11const pkg = { weight: 10, fragile: true };12calculateShipping(pkg); // OK1314// Fails15const address = { street: '123 Main' };16// calculateShipping(address); // Error: no 'weight' property
Constraints with Multiple Bounds
Combine constraints with intersection:
typescript1interface Identifiable {2 id: string;3}45interface Weighable {6 weight: number;7}89// T must have both id and weight10function processItem<T extends Identifiable & Weighable>(item: T): string {11 return `Processing ${item.id} weighing ${item.weight}kg`;12}1314const shipment = { id: 'SHP001', weight: 25, carrier: 'fedex' };15processItem(shipment); // OK - has both id and weight
The keyof Constraint
Access object properties safely:
typescript1function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {2 return obj[key];3}45const address = {6 street: '123 Main St',7 city: 'Chicago',8 postalCode: '60601'9};1011const city = getProperty(address, 'city'); // string12const zip = getProperty(address, 'postalCode'); // string13// getProperty(address, 'country'); // Error: 'country' not in Address
Generic Classes
Classes can be generic:
typescript1class Warehouse<T> {2 private items: T[] = [];34 add(item: T): void {5 this.items.push(item);6 }78 remove(): T | undefined {9 return this.items.pop();10 }1112 getAll(): T[] {13 return [...this.items];14 }15}1617// Type-safe warehouse for packages18interface Package {19 id: string;20 weight: number;21}2223const packageWarehouse = new Warehouse<Package>();24packageWarehouse.add({ id: 'PKG001', weight: 5 });25packageWarehouse.add({ id: 'PKG002', weight: 10 });2627const pkg = packageWarehouse.remove();28// pkg is Package | undefined
Default Type Parameters
Provide defaults for type parameters:
typescript1interface ApiResponse<T = unknown, E = Error> {2 data?: T;3 error?: E;4 status: number;5}67// Use default8const response: ApiResponse = { status: 200 };910// Override defaults11interface ValidationError {12 field: string;13 message: string;14}1516const validationResponse: ApiResponse<Address, ValidationError[]> = {17 status: 400,18 error: [{ field: 'postalCode', message: 'Invalid format' }]19};
Generic Pattern Example: Repository
A Repository pattern with generics:
typescript1interface Entity {2 id: string;3}45interface Repository<T extends Entity> {6 findById(id: string): T | undefined;7 findAll(): T[];8 save(entity: T): void;9 delete(id: string): boolean;10}1112// Implement for specific entity13interface Order extends Entity {14 customerId: string;15 items: string[];16 total: number;17}1819class OrderRepository implements Repository<Order> {20 private orders: Map<string, Order> = new Map();2122 findById(id: string): Order | undefined {23 return this.orders.get(id);24 }2526 findAll(): Order[] {27 return Array.from(this.orders.values());28 }2930 save(order: Order): void {31 this.orders.set(order.id, order);32 }3334 delete(id: string): boolean {35 return this.orders.delete(id);36 }37}
Common Generic Patterns
typescript1// Nullable wrapper2type Nullable<T> = T | null;34// Array type5type List<T> = T[];67// Promise wrapper8type Async<T> = Promise<T>;910// Record with specific key type11type Dictionary<T> = Record<string, T>;1213// Partial update14type Update<T> = Partial<T> & { id: string };
Summary
| Concept | Syntax | Use Case |
|---|---|---|
| Type parameter | <T> | Capture type for reuse |
| Multiple params | <K, V> | Multiple related types |
| Constraint | <T extends X> | Limit allowed types |
| Default | <T = X> | Provide fallback type |
| keyof | <K extends keyof T> | Safe property access |
Generics are fundamental to patterns like:
- Factory - Create instances of type
T - Repository - Store/retrieve entities of type
T - Builder - Build objects of type
T - Iterator - Iterate over elements of type
T
Next: Hands-on workshop building type-safe address types!