Back to Blog
TypeScriptGenericsTypesAdvanced

TypeScript Generics: Practical Patterns for Real Apps

Master TypeScript generics with real-world patterns. Learn constraints, inference, and utility types.

B
Bootspring Team
Engineering
February 27, 2026
4 min read

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 | undefined

Generic 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 number

Conditional 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>; // string

Mapped 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 }); // Typed

Use generics to eliminate type assertions, catch errors at compile time, and create flexible APIs.

Share this article

Help spread the word about Bootspring