← Back to Blog
Development2024-11-0416 min read

Advanced TypeScript Patterns: From Good to Great

Level up your TypeScript skills with advanced patterns, utility types, and techniques used in production codebases. Write type-safe, maintainable code.

MD
Manoj Dhiman
Advanced TypeScript Patterns: From Good to Great
# Advanced TypeScript Patterns: From Good to Great TypeScript has become the standard for modern JavaScript development. But there's a huge difference between basic TypeScript and advanced, type-safe code that catches bugs before they happen. Let me share the patterns that separate good TypeScript code from great. ## Why Advanced TypeScript Matters Basic TypeScript gives you autocomplete and catches obvious bugs. Advanced TypeScript: - Makes invalid states unrepresentable - Catches logic errors at compile time - Provides self-documenting code - Enables fearless refactoring - Improves developer experience ## Pattern 1: Discriminated Unions Model your domain with discriminated unions to make impossible states impossible. ### Before: ```typescript interface State { loading: boolean error: Error | null data: User | null } // Problem: data could be null while loading is false ``` ### After: ```typescript type State = | { status: 'idle' } | { status: 'loading' } | { status: 'error'; error: Error } | { status: 'success'; data: User } // Now TypeScript knows exactly what properties exist ``` ## Pattern 2: Branded Types Create nominal types for type safety: ```typescript type UserId = string & { readonly brand: unique symbol } type ProductId = string & { readonly brand: unique symbol } function getUser(id: UserId) { } function getProduct(id: ProductId) { } const userId = 'user-123' as UserId const productId = 'product-456' as ProductId getUser(productId) // Error! 🎉 ``` ## Pattern 3: Builder Pattern with Fluent API ```typescript class QueryBuilder<T> { private filters: Filter[] = [] where(condition: Filter): QueryBuilder<T> { this.filters.push(condition) return this } select<K extends keyof T>(...fields: K[]): QueryBuilder<Pick<T, K>> { return this as any } async execute(): Promise<T[]> { // Execute query } } // Usage with full type safety const users = await new QueryBuilder<User>() .where({ age: { gt: 18 } }) .select('name', 'email') .execute() // users is User[] with only name and email properties ``` ## Pattern 4: Template Literal Types ```typescript type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' type Route = '/users' | '/products' | '/orders' type API = `${HTTPMethod} ${Route}` // Valid: 'GET /users', 'POST /products', etc. // Invalid: 'PATCH /users', 'GET /invalid' function callAPI(endpoint: API) { } callAPI('GET /users') // ✅ callAPI('PATCH /users') // ❌ Error! ``` ## Pattern 5: Recursive Types ```typescript type JSONValue = | string | number | boolean | null | JSONValue[] | { [key: string]: JSONValue } const validJSON: JSONValue = { name: 'John', age: 30, hobbies: ['reading', 'coding'], address: { city: 'NYC', coordinates: [40.7128, -74.0060], }, } ``` ## Pattern 6: Conditional Types ```typescript type IsString<T> = T extends string ? true : false type A = IsString<string> // true type B = IsString<number> // false // More practical example type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T] interface User { name: string age: number save: () => void delete: () => void } type UserMethods = FunctionPropertyNames<User> // 'save' | 'delete' ``` ## Pattern 7: Mapped Types with Key Remapping ```typescript type Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] } type Person = { name: string age: number } type PersonGetters = Getters<Person> // { // getName: () => string // getAge: () => number // } ``` ## Pattern 8: Utility Type Combinations ```typescript // Make specific properties optional type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>> interface User { id: string name: string email: string age: number } type UserUpdate = PartialBy<User, 'name' | 'age'> // id and email required, name and age optional // Deep readonly type DeepReadonly<T> = { readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K] } ``` ## Pattern 9: Type Guards ```typescript interface Dog { type: 'dog' bark: () => void } interface Cat { type: 'cat' meow: () => void } type Animal = Dog | Cat function isDog(animal: Animal): animal is Dog { return animal.type === 'dog' } function handleAnimal(animal: Animal) { if (isDog(animal)) { animal.bark() // TypeScript knows it's a Dog } else { animal.meow() // TypeScript knows it's a Cat } } ``` ## Pattern 10: Generic Constraints ```typescript interface Identifiable { id: string } function findById<T extends Identifiable>( items: T[], id: string ): T | undefined { return items.find(item => item.id === id) } // Works with any type that has an id const user = findById(users, 'user-123') const product = findById(products, 'product-456') ``` ## Real-World Example: Type-Safe API Client ```typescript type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' interface APIEndpoint { method: HTTPMethod path: string body?: unknown response: unknown } type API = { 'GET /users': { method: 'GET' path: '/users' response: User[] } 'GET /users/:id': { method: 'GET' path: '/users/:id' response: User } 'POST /users': { method: 'POST' path: '/users' body: CreateUserDTO response: User } } type ExtractParams<T extends string> = T extends `${infer Start}:${infer Param}/${infer Rest}` ? { [K in Param | keyof ExtractParams<Rest>]: string } : T extends `${infer Start}:${infer Param}` ? { [K in Param]: string } : {} async function api<K extends keyof API>( endpoint: K, options: { params?: ExtractParams<API[K]['path']> body?: API[K] extends { body: infer B } ? B : never } = {} ): Promise<API[K]['response']> { // Implementation } // Fully type-safe usage const users = await api('GET /users') // User[] const user = await api('GET /users/:id', { params: { id: '123' } }) // User const newUser = await api('POST /users', { body: { name: 'John', email: 'john@example.com' } }) // User ``` ## Best Practices ### 1. Prefer Type Inference ```typescript // Bad const numbers: number[] = [1, 2, 3] // Good const numbers = [1, 2, 3] // TypeScript infers number[] ``` ### 2. Use `const` Assertions ```typescript const config = { api: 'https://api.example.com', timeout: 5000, } as const // config.api is 'https://api.example.com', not string ``` ### 3. Avoid Type Assertions ```typescript // Bad const user = data as User // Good function isUser(data: unknown): data is User { return typeof data === 'object' && data !== null && 'id' in data } if (isUser(data)) { // data is User } ``` ### 4. Use Unknown Over Any ```typescript // Bad function process(data: any) { data.whatever() // No type checking } // Good function process(data: unknown) { if (typeof data === 'object' && data !== null) { // Now we can safely work with data } } ``` ## Performance Considerations 1. **Avoid Complex Unions**: Union types with 1000+ members slow down compilation 2. **Use Module Augmentation Sparingly**: Can impact compilation performance 3. **Prefer Interfaces Over Types**: For object shapes, interfaces perform better 4. **Use Project References**: For large monorepos ## Conclusion Advanced TypeScript patterns transform your code from "has types" to "impossible to misuse." These patterns catch bugs at compile time, provide better IDE support, and make your code self-documenting. Start incorporating these patterns gradually. Your future self (and teammates) will thank you. Remember: The goal isn't to use every advanced feature, but to write code that's impossible to use incorrectly. Happy typing! 🎯
#TypeScript#Advanced#Type Safety#Patterns#Best Practices

Related Articles