Generics enable reusable, type-safe code. Here are practical patterns you'll use daily.
Basic Generic Constraints#
1// Constrain to objects with id
2interface HasId {
3 id: string | number;
4}
5
6function findById<T extends HasId>(items: T[], id: T['id']): T | undefined {
7 return items.find(item => item.id === id);
8}
9
10const users = [{ id: 1, name: 'John' }];
11findById(users, 1); // Works, returns User | undefinedGeneric Factory Functions#
1function createStore<T>(initialValue: T) {
2 let value = initialValue;
3
4 return {
5 get: () => value,
6 set: (newValue: T) => { value = newValue; },
7 update: (updater: (current: T) => T) => {
8 value = updater(value);
9 },
10 };
11}
12
13const counterStore = createStore(0);
14counterStore.set(5); // OK
15counterStore.set('five'); // Error: string not assignable to numberConditional Types#
1// Extract return type of async functions
2type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
3
4type Result = UnwrapPromise<Promise<string>>; // string
5type Direct = UnwrapPromise<number>; // number
6
7// Exclude null and undefined
8type NonNullable<T> = T extends null | undefined ? never : T;
9
10type Clean = NonNullable<string | null>; // stringMapped Types#
1// Make all properties optional
2type Partial<T> = {
3 [K in keyof T]?: T[K];
4};
5
6// Make all properties required
7type Required<T> = {
8 [K in keyof T]-?: T[K];
9};
10
11// Pick specific properties
12type Pick<T, K extends keyof T> = {
13 [P in K]: T[P];
14};
15
16// Create readonly version
17type Readonly<T> = {
18 readonly [K in keyof T]: T[K];
19};Template Literal Types#
1type HttpMethod = 'get' | 'post' | 'put' | 'delete';
2type ApiRoute = `/api/${string}`;
3
4type EndpointKey = `${HttpMethod}:${ApiRoute}`;
5// "get:/api/users" | "post:/api/users" | etc.
6
7// Event handler names
8type EventName = 'click' | 'focus' | 'blur';
9type Handler = `on${Capitalize<EventName>}`;
10// "onClick" | "onFocus" | "onBlur"Generic React Components#
1interface ListProps<T> {
2 items: T[];
3 renderItem: (item: T) => React.ReactNode;
4 keyExtractor: (item: T) => string;
5}
6
7function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
8 return (
9 <ul>
10 {items.map(item => (
11 <li key={keyExtractor(item)}>{renderItem(item)}</li>
12 ))}
13 </ul>
14 );
15}
16
17// Usage with type inference
18<List
19 items={users}
20 renderItem={(user) => user.name} // user is inferred as User
21 keyExtractor={(user) => user.id}
22/>Builder Pattern with Generics#
1class QueryBuilder<T extends object> {
2 private filters: Partial<T> = {};
3 private sortField?: keyof T;
4
5 where<K extends keyof T>(field: K, value: T[K]): this {
6 this.filters[field] = value;
7 return this;
8 }
9
10 orderBy(field: keyof T): this {
11 this.sortField = field;
12 return this;
13 }
14
15 build() {
16 return { filters: this.filters, sort: this.sortField };
17 }
18}
19
20interface User {
21 name: string;
22 age: number;
23 active: boolean;
24}
25
26new QueryBuilder<User>()
27 .where('age', 25) // OK
28 .where('name', 'John') // OK
29 .where('age', 'twenty') // Error: string not assignable to number
30 .orderBy('name')
31 .build();Discriminated Unions#
1type Result<T, E = Error> =
2 | { success: true; data: T }
3 | { success: false; error: E };
4
5function handleResult<T>(result: Result<T>) {
6 if (result.success) {
7 console.log(result.data); // T is available
8 } else {
9 console.error(result.error); // Error is available
10 }
11}Type-Safe Event Emitter#
1type EventMap = {
2 userCreated: { userId: string; email: string };
3 orderPlaced: { orderId: string; total: number };
4};
5
6class TypedEmitter<T extends Record<string, unknown>> {
7 private listeners = new Map<keyof T, Set<Function>>();
8
9 on<K extends keyof T>(event: K, handler: (data: T[K]) => void) {
10 if (!this.listeners.has(event)) {
11 this.listeners.set(event, new Set());
12 }
13 this.listeners.get(event)!.add(handler);
14 }
15
16 emit<K extends keyof T>(event: K, data: T[K]) {
17 this.listeners.get(event)?.forEach(fn => fn(data));
18 }
19}
20
21const emitter = new TypedEmitter<EventMap>();
22emitter.on('userCreated', ({ userId, email }) => { }); // Typed
23emitter.emit('orderPlaced', { orderId: '123', total: 99 }); // TypedUse generics to eliminate type assertions, catch errors at compile time, and create flexible APIs.