Frontend Engineering

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.

Khalid Aboubakr
16 min read
TypescriptType SafetyPatternsGenericsUtility TypesType GuardsDiscriminated Unions

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:

  1. Discriminated unions prevent invalid states
  2. Template literal types create precise string types
  3. Builder patterns enable fluent, type-safe APIs
  4. API client types catch contract violations at compile time
  5. 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.