Advanced TypeScript Patterns for Large Codebases
Master advanced TypeScript patterns including generics, utility types, and type guards. Real examples from large-scale React and Node.js codebases.
Introduction
TypeScript's type system is far more powerful than basic type annotations suggest. When building large codebases, advanced patterns can catch entire categories of bugs at compile time, make refactoring safer, and serve as living documentation.
Pattern 1: Discriminated Unions for State Machines
Model complex state with compile-time exhaustiveness checking:
// ❌ Naive approach: optional fields lead to invalid states interface ApiState { loading?: boolean; data?: User[]; error?: Error; } // Problem: { loading: true, data: [...], error: new Error() } is valid // ✅ Discriminated union: only valid states are representable type ApiState<T> = | { status: 'idle' } | { status: 'loading' } | { status: 'success'; data: T } | { status: 'error'; error: Error }; function renderUsers(state: ApiState<User[]>) { switch (state.status) { case 'idle': return <p>Click to load users</p>; case 'loading': return <Spinner />; case 'success': return <UserList users={state.data} />; // TypeScript knows data exists case 'error': return <ErrorMessage error={state.error} />; // If we miss a case, TypeScript errors } }
Pattern 2: Template Literal Types
Create precise string types:
// Type-safe event names type EventName<T extends string> = `on${Capitalize<T>}`; type ClickHandler = EventName<'click'>; // "onClick" // Route parameters type ExtractParams<T extends string> = T extends `${string}:${infer Param}/${infer Rest}` ? Param | ExtractParams<Rest> : T extends `${string}:${infer Param}` ? Param : never; type UserRouteParams = ExtractParams<'/users/:userId/posts/:postId'>; // "userId" | "postId" // Type-safe route builder function buildPath<T extends string>( template: T, params: Record<ExtractParams<T>, string> ): string { let path: string = template; for (const [key, value] of Object.entries(params)) { path = path.replace(`:${key}`, value); } return path; } // Usage - TypeScript ensures all params are provided const path = buildPath('/users/:userId/posts/:postId', { userId: '123', postId: '456', }); // "/users/123/posts/456"
Pattern 3: Builder Pattern with Method Chaining
Type-safe fluent APIs:
class QueryBuilder<T, Selected = T> { private conditions: string[] = []; private selectedFields: string[] = []; where<K extends keyof T>( field: K, operator: '=' | '>' | '<' | 'LIKE', value: T[K] ): QueryBuilder<T, Selected> { this.conditions.push(`${String(field)} ${operator} '${value}'`); return this; } select<K extends keyof T>(...fields: K[]): QueryBuilder<T, Pick<T, K>> { this.selectedFields = fields.map(String); return this as unknown as QueryBuilder<T, Pick<T, K>>; } execute(): Promise<Selected[]> { const query = this.buildQuery(); return this.db.query(query); } } // Usage interface User { id: number; name: string; email: string; age: number; } const users = await new QueryBuilder<User>() .select('id', 'name') .where('age', '>', 18) .execute(); // users is typed as Pick<User, 'id' | 'name'>[]
Pattern 4: Type-Safe API Client
Generate types from API schema:
// Define API contract interface ApiRoutes { 'GET /users': { response: User[]; query: { page?: number; limit?: number }; }; 'GET /users/:id': { response: User; params: { id: string }; }; 'POST /users': { response: User; body: CreateUserDto; }; 'PUT /users/:id': { response: User; params: { id: string }; body: UpdateUserDto; }; } // Type-safe client type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'; type RouteConfig<T> = T extends { params: infer P } ? { params: P } : {} & T extends { query: infer Q } ? { query: Q } : {} & T extends { body: infer B } ? { body: B } : {}; async function apiClient< Route extends keyof ApiRoutes, Config extends ApiRoutes[Route] >( route: Route, config: RouteConfig<Config> ): Promise<Config['response']> { // Implementation } // Usage - fully type-safe const users = await apiClient('GET /users', { query: { page: 1 } }); const user = await apiClient('GET /users/:id', { params: { id: '123' } });
Pattern 5: Branded Types
Prevent mixing similar primitive types:
// Problem: easy to mix up IDs function assignTask(taskId: string, userId: string) { /* ... */ } assignTask(userId, taskId); // Oops! No type error // Solution: branded types type Brand<T, B> = T & { __brand: B }; type UserId = Brand<string, 'UserId'>; type TaskId = Brand<string, 'TaskId'>; function createUserId(id: string): UserId { return id as UserId; } function createTaskId(id: string): TaskId { return id as TaskId; } function assignTask(taskId: TaskId, userId: UserId) { /* ... */ } const userId = createUserId('user-123'); const taskId = createTaskId('task-456'); assignTask(taskId, userId); // ✅ Correct assignTask(userId, taskId); // ❌ Type error!
Conclusion
Advanced TypeScript patterns transform your codebase:
- Discriminated unions prevent invalid states
- Template literal types create precise string types
- Builder patterns enable fluent, type-safe APIs
- API client types catch contract violations at compile time
- Branded types prevent mixing similar primitives
The investment in type safety pays dividends through fewer bugs, safer refactoring, and better developer experience.
Related Articles
Frontend Engineering17 min read
React Performance Optimization at Scale
Optimize React performance with useMemo, useCallback, and code splitting. Real performance metrics and patterns from large-scale production applications.
Frontend Engineering15 min read
State Management Architecture in Modern React
Compare React state management solutions: Redux Toolkit, Zustand, and Context API. Learn when to use each with real examples and performance considerations.
Backend Design19 min read
API Design: Choosing Between REST, GraphQL, and gRPC
Compare REST, GraphQL, and gRPC APIs with performance benchmarks and use cases. Learn which API style fits your project based on real production experience.
Backend Design18 min read
Laravel at Scale: Enterprise Patterns Beyond MVC
Build enterprise Laravel applications with repository pattern, service layer, and DDD principles. Production patterns from government and healthcare systems.
Security Engineering18 min read
API Security Hardening: A Practitioner's Guide
Secure your APIs with rate limiting, input validation, and CORS configuration. Production-tested checklist covering authentication, encryption, and error handling.