20 minlesson

Generics for Reusable Patterns

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:

typescript
1// Using 'any' loses type information
2function firstElement(arr: any[]): any {
3 return arr[0];
4}
5
6const packages = [{ weight: 5 }, { weight: 10 }];
7const first = firstElement(packages);
8// first is 'any' - no autocomplete, no type checking
9console.log(first.wieght); // Typo not caught!

Generic Functions

Generics preserve type information:

typescript
1function firstElement<T>(arr: T[]): T | undefined {
2 return arr[0];
3}
4
5// Type is inferred
6const packages = [{ weight: 5 }, { weight: 10 }];
7const first = firstElement(packages);
8// first is { weight: number } | undefined
9console.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:

typescript
1function createPair<K, V>(key: K, value: V): [K, V] {
2 return [key, value];
3}
4
5// Types inferred from arguments
6const pair = createPair('trackingId', 'TRK123456');
7// pair is [string, string]
8
9const entry = createPair(1, { status: 'delivered' });
10// entry is [number, { status: string }]

Generic Interfaces

Interfaces can be generic too:

typescript
1// Generic container
2interface Container<T> {
3 contents: T;
4 addItem(item: T): void;
5 getItem(): T;
6}
7
8// Generic result type
9interface Result<T, E = Error> {
10 success: boolean;
11 data?: T;
12 error?: E;
13}
14
15// Usage
16interface Package {
17 id: string;
18 weight: number;
19}
20
21const 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:

typescript
1// T must have a 'weight' property
2interface Weighable {
3 weight: number;
4}
5
6function calculateShipping<T extends Weighable>(item: T): number {
7 return item.weight * 2.5; // Safe: weight is guaranteed
8}
9
10// Works
11const pkg = { weight: 10, fragile: true };
12calculateShipping(pkg); // OK
13
14// Fails
15const address = { street: '123 Main' };
16// calculateShipping(address); // Error: no 'weight' property

Constraints with Multiple Bounds

Combine constraints with intersection:

typescript
1interface Identifiable {
2 id: string;
3}
4
5interface Weighable {
6 weight: number;
7}
8
9// T must have both id and weight
10function processItem<T extends Identifiable & Weighable>(item: T): string {
11 return `Processing ${item.id} weighing ${item.weight}kg`;
12}
13
14const shipment = { id: 'SHP001', weight: 25, carrier: 'fedex' };
15processItem(shipment); // OK - has both id and weight

The keyof Constraint

Access object properties safely:

typescript
1function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
2 return obj[key];
3}
4
5const address = {
6 street: '123 Main St',
7 city: 'Chicago',
8 postalCode: '60601'
9};
10
11const city = getProperty(address, 'city'); // string
12const zip = getProperty(address, 'postalCode'); // string
13// getProperty(address, 'country'); // Error: 'country' not in Address

Generic Classes

Classes can be generic:

typescript
1class Warehouse<T> {
2 private items: T[] = [];
3
4 add(item: T): void {
5 this.items.push(item);
6 }
7
8 remove(): T | undefined {
9 return this.items.pop();
10 }
11
12 getAll(): T[] {
13 return [...this.items];
14 }
15}
16
17// Type-safe warehouse for packages
18interface Package {
19 id: string;
20 weight: number;
21}
22
23const packageWarehouse = new Warehouse<Package>();
24packageWarehouse.add({ id: 'PKG001', weight: 5 });
25packageWarehouse.add({ id: 'PKG002', weight: 10 });
26
27const pkg = packageWarehouse.remove();
28// pkg is Package | undefined

Default Type Parameters

Provide defaults for type parameters:

typescript
1interface ApiResponse<T = unknown, E = Error> {
2 data?: T;
3 error?: E;
4 status: number;
5}
6
7// Use default
8const response: ApiResponse = { status: 200 };
9
10// Override defaults
11interface ValidationError {
12 field: string;
13 message: string;
14}
15
16const validationResponse: ApiResponse<Address, ValidationError[]> = {
17 status: 400,
18 error: [{ field: 'postalCode', message: 'Invalid format' }]
19};

Generic Pattern Example: Repository

A Repository pattern with generics:

typescript
1interface Entity {
2 id: string;
3}
4
5interface Repository<T extends Entity> {
6 findById(id: string): T | undefined;
7 findAll(): T[];
8 save(entity: T): void;
9 delete(id: string): boolean;
10}
11
12// Implement for specific entity
13interface Order extends Entity {
14 customerId: string;
15 items: string[];
16 total: number;
17}
18
19class OrderRepository implements Repository<Order> {
20 private orders: Map<string, Order> = new Map();
21
22 findById(id: string): Order | undefined {
23 return this.orders.get(id);
24 }
25
26 findAll(): Order[] {
27 return Array.from(this.orders.values());
28 }
29
30 save(order: Order): void {
31 this.orders.set(order.id, order);
32 }
33
34 delete(id: string): boolean {
35 return this.orders.delete(id);
36 }
37}

Common Generic Patterns

typescript
1// Nullable wrapper
2type Nullable<T> = T | null;
3
4// Array type
5type List<T> = T[];
6
7// Promise wrapper
8type Async<T> = Promise<T>;
9
10// Record with specific key type
11type Dictionary<T> = Record<string, T>;
12
13// Partial update
14type Update<T> = Partial<T> & { id: string };

Summary

ConceptSyntaxUse 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!