Advanced TypeScript Patterns for Scalable Applications
TypeScript has evolved far beyond simple type annotations. Modern TypeScript offers powerful type-level programming capabilities that can encode business logic, prevent runtime errors, and improve developer experience. Here are the advanced patterns that will level up your TypeScript game.
Branded Types: Beyond Primitive Obsession
Branded types help you avoid the primitive obsession antipattern by creating distinct types from primitives.
// Without branded types - dangerous! function transferMoney(fromAccount: string, toAccount: string, amount: number) { // Easy to accidentally swap parameters // transferMoney(amount, fromAccount, toAccount) // Runtime bug! } // With branded types - type safe! type AccountId = string & { readonly brand: unique symbol }; type UserId = string & { readonly brand: unique symbol }; type Money = number & { readonly brand: unique symbol }; function createAccountId(id: string): AccountId { return id as AccountId; } function createMoney(amount: number): Money { if (amount < 0) throw new Error('Money cannot be negative'); return amount as Money; } function transferMoney( fromAccount: AccountId, toAccount: AccountId, amount: Money ) { // Type system prevents parameter confusion // transferMoney(amount, fromAccount, toAccount) // Compile error! }
Real-world Application
// Domain-specific branded types type EmailAddress = string & { readonly brand: unique symbol }; type PhoneNumber = string & { readonly brand: unique symbol }; type PostalCode = string & { readonly brand: unique symbol }; // Factory functions with validation function createEmail(email: string): EmailAddress { if (!email.includes('@')) { throw new Error('Invalid email format'); } return email as EmailAddress; } // Now impossible to mix up different string types class User { constructor( public email: EmailAddress, public phone: PhoneNumber, public postalCode: PostalCode ) {} }
Template Literal Types: Type-Safe String Manipulation
Template literal types allow you to manipulate string types at the type level.
// Create CSS property types type CSSUnit = 'px' | 'em' | 'rem' | '%' | 'vh' | 'vw'; type CSSValue<T extends string> = `${T}${CSSUnit}`; type Width = CSSValue<'auto'> | CSSValue<string>; type Height = CSSValue<'auto'> | CSSValue<string>; // Usage const width: Width = '100px'; // ✅ const height: Height = '50vh'; // ✅ const invalid: Width = '100'; // ❌ Type error // API route type generation type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'; type APIEndpoint = 'users' | 'posts' | 'comments'; type APIRoute = `/${APIEndpoint}` | `/${APIEndpoint}/${string}`; // Generate type-safe route handlers type RouteHandler<T extends APIRoute> = ( req: Request & { route: T } ) => Promise<Response>; const getUsersHandler: RouteHandler<'/users'> = async (req) => { // req.route is guaranteed to be '/users' return new Response('Users data'); };
Advanced String Manipulation
// Capitalize utility type type Capitalize<S extends string> = S extends `${infer First}${infer Rest}` ? `${Uppercase<First>}${Rest}` : S; // Snake case to camel case type SnakeToCamel<S extends string> = S extends `${infer Head}_${infer Tail}` ? `${Head}${Capitalize<SnakeToCamel<Tail>>}` : S; type DatabaseColumns = 'user_id' | 'first_name' | 'last_name' | 'email_address'; type APIFields = SnakeToCamel<DatabaseColumns>; // Result: 'userId' | 'firstName' | 'lastName' | 'emailAddress'
Conditional Types: Logic at the Type Level
Conditional types enable type-level branching logic.
// Smart API response types type APIResponse<T> = T extends { error: any } ? { success: false; error: T['error'] } : { success: true; data: T }; // Usage type UserResponse = APIResponse<{ id: number; name: string }>; // Result: { success: true; data: { id: number; name: string } } type ErrorResponse = APIResponse<{ error: string }>; // Result: { success: false; error: string } // Function overloads with conditional types type EventMap = { click: MouseEvent; keydown: KeyboardEvent; scroll: Event; }; function addEventListener<K extends keyof EventMap>( element: Element, type: K, listener: (event: EventMap[K]) => void ): void; // TypeScript knows the exact event type addEventListener(button, 'click', (event) => { // event is MouseEvent, has clientX, clientY, etc. console.log(event.clientX); });
Advanced Conditional Type Patterns
// Deep readonly utility type DeepReadonly<T> = { readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P]; }; interface User { id: number; profile: { name: string; settings: { theme: string; notifications: boolean; }; }; } type ReadonlyUser = DeepReadonly<User>; // All nested properties are readonly
Mapped Types: Transforming Object Types
Mapped types allow you to create new types by transforming existing ones.
// Make all properties optional type Partial<T> = { [P in keyof T]?: T[P]; }; // Make specific properties required type RequiredFields<T, K extends keyof T> = T & Required<Pick<T, K>>; interface CreateUserRequest { name?: string; email?: string; password?: string; } // Ensure email and password are required type ValidatedCreateUserRequest = RequiredFields< CreateUserRequest, 'email' | 'password' >; // Result: { name?: string; email: string; password: string; }
Advanced Mapped Type Patterns
// Create getters for all properties type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]; }; interface User { id: number; name: string; email: string; } type UserGetters = Getters<User>; // Result: { // getId: () => number; // getName: () => string; // getEmail: () => string; // } // Filter properties by type type FilterByType<T, U> = { [K in keyof T as T[K] extends U ? K : never]: T[K]; }; interface MixedProps { id: number; name: string; count: number; isActive: boolean; } type StringProps = FilterByType<MixedProps, string>; // Result: { name: string; } type NumberProps = FilterByType<MixedProps, number>; // Result: { id: number; count: number; }
Builder Pattern with Fluent Types
Create type-safe builder patterns that guide the construction process.
interface DatabaseConfig { host: string; port: number; database: string; username: string; password: string; ssl?: boolean; } class DatabaseConfigBuilder { private config: Partial<DatabaseConfig> = {}; host(host: string): DatabaseConfigBuilder & { _host: true } { this.config.host = host; return this as any; } port(port: number): DatabaseConfigBuilder & { _port: true } { this.config.port = port; return this as any; } database(database: string): DatabaseConfigBuilder & { _database: true } { this.config.database = database; return this as any; } username(username: string): DatabaseConfigBuilder & { _username: true } { this.config.username = username; return this as any; } password(password: string): DatabaseConfigBuilder & { _password: true } { this.config.password = password; return this as any; } ssl(enabled: boolean = true): DatabaseConfigBuilder { this.config.ssl = enabled; return this; } build( this: DatabaseConfigBuilder & { _host: true } & { _port: true } & { _database: true } & { _username: true } & { _password: true } ): DatabaseConfig { return this.config as DatabaseConfig; } } // Usage - TypeScript enforces required fields const config = new DatabaseConfigBuilder() .host('localhost') .port(5432) .database('myapp') .username('admin') .password('secret') .ssl(true) .build(); // ✅ All required fields provided // This would cause a compile error: // const invalidConfig = new DatabaseConfigBuilder() // .host('localhost') // .build(); // ❌ Missing required fields
State Machine Types
Model complex state transitions with TypeScript.
// Order state machine type OrderState = | { status: 'pending'; orderId: string } | { status: 'processing'; orderId: string; paymentId: string } | { status: 'shipped'; orderId: string; trackingNumber: string } | { status: 'delivered'; orderId: string; deliveredAt: Date } | { status: 'cancelled'; orderId: string; reason: string }; // Type-safe state transitions class OrderStateMachine { constructor(private state: OrderState) {} process( this: OrderStateMachine & { state: Extract<OrderState, { status: 'pending' }> }, paymentId: string ): OrderStateMachine { return new OrderStateMachine({ status: 'processing', orderId: this.state.orderId, paymentId }); } ship( this: OrderStateMachine & { state: Extract<OrderState, { status: 'processing' }> }, trackingNumber: string ): OrderStateMachine { return new OrderStateMachine({ status: 'shipped', orderId: this.state.orderId, trackingNumber }); } deliver( this: OrderStateMachine & { state: Extract<OrderState, { status: 'shipped' }> } ): OrderStateMachine { return new OrderStateMachine({ status: 'delivered', orderId: this.state.orderId, deliveredAt: new Date() }); } getState(): OrderState { return this.state; } } // Usage - invalid transitions caught at compile time const order = new OrderStateMachine({ status: 'pending', orderId: '123' }); const processing = order.process('payment-456'); const shipped = processing.ship('tracking-789'); const delivered = shipped.deliver(); // This would be a compile error: // order.ship('tracking-123'); // ❌ Can't ship a pending order
Performance and Best Practices
Type Assertion vs Type Guards
// Type assertion - dangerous function processUser(data: unknown) { const user = data as User; // No runtime validation! return user.email.toLowerCase(); } // Type guard - safe function isUser(data: unknown): data is User { return ( typeof data === 'object' && data !== null && 'email' in data && typeof (data as any).email === 'string' ); } function processUserSafely(data: unknown) { if (isUser(data)) { return data.email.toLowerCase(); // TypeScript knows data is User } throw new Error('Invalid user data'); }
Utility Type Composition
// Compose utility types for complex transformations type APIEntity<T> = { id: string; createdAt: Date; updatedAt: Date; } & T; type CreateRequest<T> = Omit<T, 'id' | 'createdAt' | 'updatedAt'>; type UpdateRequest<T> = Partial<CreateRequest<T>>; interface User { name: string; email: string; role: 'admin' | 'user'; } type UserEntity = APIEntity<User>; type CreateUserRequest = CreateRequest<UserEntity>; type UpdateUserRequest = UpdateRequest<UserEntity>;
Testing with Advanced Types
// Type-safe test builders type TestBuilder<T> = { [K in keyof T]: (value: T[K]) => TestBuilder<T>; } & { build(): T; }; function createTestBuilder<T>(defaults: T): TestBuilder<T> { const data = { ...defaults }; const builder = {} as TestBuilder<T>; for (const key in defaults) { (builder as any)[key] = (value: T[typeof key]) => { data[key] = value; return builder; }; } builder.build = () => ({ ...data }); return builder; } // Usage in tests const userBuilder = createTestBuilder({ id: '1', name: 'Test User', email: '[email protected]', role: 'user' as const }); const testUser = userBuilder .name('John Doe') .email('[email protected]') .build();
These advanced TypeScript patterns help you write more robust, maintainable code by encoding business logic and constraints directly in the type system. The key is to start simple and gradually adopt these patterns as your application grows in complexity.
Remember: types should serve your code, not the other way around. Use these patterns when they solve real problems, not just because they're clever.
Which TypeScript patterns have you found most useful in your projects? I'd love to hear about your experiences with advanced type programming.