TypeScript has evolved far beyond simple type annotations. Modern TypeScript offers a sophisticated type system that can catch complex bugs at compile time and provide incredible developer experience. Let’s explore advanced patterns that will elevate your TypeScript skills.
The Evolution of TypeScript’s Type System
TypeScript’s type system has become incredibly powerful, allowing us to express complex relationships and constraints that were previously impossible. These advanced patterns aren’t just academic exercises—they’re practical tools for building more reliable software.
Why Advanced Types Matter
1// Basic typing - good start
2interface User {
3 id: string;
4 name: string;
5 email: string;
6}
7
8// Advanced typing - catches more bugs
9interface User {
10 readonly id: UUID;
11 name: NonEmptyString;
12 email: Email;
13 createdAt: Timestamp;
14 updatedAt: Timestamp;
15}
16
17type UUID = string & { readonly brand: unique symbol };
18type NonEmptyString = string & { readonly brand: unique symbol };
19type Email = string & { readonly brand: unique symbol };
20type Timestamp = number & { readonly brand: unique symbol };
Pattern 1: Mapped Types for API Transformations
Mapped types allow you to create new types by transforming existing ones. This is incredibly useful for API responses and data transformations.
Basic Mapped Types
1// Transform all properties to optional
2type Partial<T> = {
3 [P in keyof T]?: T[P];
4};
5
6// Transform all properties to required
7type Required<T> = {
8 [P in keyof T]-?: T[P];
9};
10
11// Real-world example: API response transformation
12interface DatabaseUser {
13 id: number;
14 firstName: string;
15 lastName: string;
16 email: string;
17 createdAt: Date;
18 updatedAt: Date;
19}
20
21// Transform for API response
22type APIUser = {
23 [K in keyof DatabaseUser]: DatabaseUser[K] extends Date
24 ? string
25 : DatabaseUser[K];
26};
27// Result: { id: number; firstName: string; ...; createdAt: string; updatedAt: string; }
Advanced Mapped Types with Key Remapping
1// Remove specific properties
2type OmitProperties<T, K extends keyof T> = {
3 [P in keyof T as P extends K ? never : P]: T[P];
4};
5
6// Prefix all property names
7type PrefixProperties<T, Prefix extends string> = {
8 [K in keyof T as `${Prefix}${Capitalize<string & K>}`]: T[K];
9};
10
11// Example usage
12interface Config {
13 apiUrl: string;
14 timeout: number;
15 retries: number;
16}
17
18type EnvironmentConfig = PrefixProperties<Config, 'env'>;
19// Result: { envApiUrl: string; envTimeout: number; envRetries: number; }
Pattern 2: Conditional Types for Smart Inference
Conditional types enable type-level programming, allowing types to change based on conditions.
Basic Conditional Types
1type IsArray<T> = T extends any[] ? true : false;
2
3type Test1 = IsArray<string[]>; // true
4type Test2 = IsArray<string>; // false
5
6// More practical example: API response handling
7type APIResponse<T> = T extends { error: any }
8 ? { success: false; error: T['error'] }
9 : { success: true; data: T };
10
11// Usage
12type UserResponse = APIResponse<{ id: string; name: string }>;
13// Result: { success: true; data: { id: string; name: string } }
14
15type ErrorResponse = APIResponse<{ error: string }>;
16// Result: { success: false; error: string }
Distributed Conditional Types
1// Extract array element types
2type Flatten<T> = T extends (infer U)[] ? U : T;
3
4type StringArray = Flatten<string[]>; // string
5type NumberType = Flatten<number>; // number
6
7// Practical example: Event handler types
8type EventMap = {
9 click: MouseEvent;
10 keydown: KeyboardEvent;
11 resize: Event;
12};
13
14type EventHandler<T extends keyof EventMap> = (event: EventMap[T]) => void;
15
16// Type-safe event handlers
17const handleClick: EventHandler<'click'> = event => {
18 // event is properly typed as MouseEvent
19 console.log(event.clientX, event.clientY);
20};
Pattern 3: Template Literal Types for String Manipulation
Template literal types provide compile-time string manipulation capabilities.
Building Dynamic Property Names
1type EventName<T extends string> = `on${Capitalize<T>}`;
2type CSSProperty<T extends string> = `--${T}`;
3
4// Create event handler types
5type ButtonEvents = EventName<'click' | 'hover' | 'focus'>;
6// Result: "onClick" | "onHover" | "onFocus"
7
8// CSS custom properties
9type ThemeProperties = CSSProperty<'primary-color' | 'secondary-color'>;
10// Result: "--primary-color" | "--secondary-color"
Advanced String Manipulation
1// Convert camelCase to kebab-case
2type CamelToKebab<S extends string> = S extends `${infer T}${infer U}`
3 ? `${T extends Capitalize<T> ? '-' : ''}${Lowercase<T>}${CamelToKebab<U>}`
4 : S;
5
6type KebabCase = CamelToKebab<'backgroundColor'>;
7// Result: "background-color"
8
9// URL parameter extraction
10type ExtractRouteParams<T extends string> =
11 T extends `${string}:${infer Param}/${infer Rest}`
12 ? Param | ExtractRouteParams<Rest>
13 : T extends `${string}:${infer Param}`
14 ? Param
15 : never;
16
17type RouteParams = ExtractRouteParams<'/users/:userId/posts/:postId'>;
18// Result: "userId" | "postId"
Pattern 4: Recursive Types for Complex Data Structures
TypeScript supports recursive types, enabling sophisticated data structure modeling.
JSON Schema Validation
1type JSONValue =
2 | string
3 | number
4 | boolean
5 | null
6 | JSONValue[]
7 | { [key: string]: JSONValue };
8
9// Type-safe JSON parsing
10function parseJSON<T extends JSONValue>(json: string): T {
11 return JSON.parse(json) as T;
12}
13
14// Usage with strong typing
15interface APIResponse {
16 users: Array<{
17 id: number;
18 name: string;
19 metadata: JSONValue;
20 }>;
21}
22
23const response = parseJSON<APIResponse>('{"users": [...]}');
Tree Structures
1interface TreeNode<T> {
2 value: T;
3 children?: TreeNode<T>[];
4}
5
6// Type-safe tree traversal
7function traverseTree<T>(
8 node: TreeNode<T>,
9 callback: (value: T) => void
10): void {
11 callback(node.value);
12 node.children?.forEach(child => traverseTree(child, callback));
13}
14
15// Usage
16const fileTree: TreeNode<string> = {
17 value: 'src',
18 children: [
19 { value: 'components', children: [{ value: 'Button.tsx' }] },
20 { value: 'utils', children: [{ value: 'helpers.ts' }] },
21 ],
22};
23
24traverseTree(fileTree, filename => console.log(filename));
Pattern 5: Brand Types for Enhanced Type Safety
Brand types prevent accidentally mixing similar primitive types.
Implementing Brand Types
1declare const __brand: unique symbol;
2
3type Brand<T, TBrand extends string> = T & {
4 readonly [__brand]: TBrand;
5};
6
7// Create branded types
8type UserId = Brand<string, 'UserId'>;
9type ProductId = Brand<string, 'ProductId'>;
10type Email = Brand<string, 'Email'>;
11
12// Constructor functions
13const createUserId = (id: string): UserId => id as UserId;
14const createProductId = (id: string): ProductId => id as ProductId;
15const createEmail = (email: string): Email => {
16 if (!email.includes('@')) {
17 throw new Error('Invalid email format');
18 }
19 return email as Email;
20};
21
22// Type-safe usage
23function getUserById(userId: UserId): Promise<User> {
24 // Implementation
25 return fetch(`/api/users/${userId}`).then(r => r.json());
26}
27
28function getProductById(productId: ProductId): Promise<Product> {
29 // Implementation
30 return fetch(`/api/products/${productId}`).then(r => r.json());
31}
32
33// This prevents bugs!
34const userId = createUserId('user-123');
35const productId = createProductId('product-456');
36
37getUserById(userId); // ✅ Correct
38getUserById(productId); // ❌ Type error - prevents bug!
Pattern 6: Function Overloading with Conditional Types
Create flexible APIs that adapt based on input types.
Dynamic Return Types
1interface SearchOptions {
2 includeDeleted?: boolean;
3 limit?: number;
4}
5
6function search<T extends SearchOptions>(
7 query: string,
8 options?: T
9): T extends { includeDeleted: true }
10 ? Array<{ item: any; deleted: boolean }>
11 : Array<any>;
12
13function search(query: string, options: SearchOptions = {}) {
14 // Implementation here
15 if (options.includeDeleted) {
16 return []; // Returns items with deleted flag
17 }
18 return []; // Returns regular items
19}
20
21// Usage with smart return type inference
22const regularResults = search('test'); // Array<any>
23const withDeleted = search('test', { includeDeleted: true }); // Array<{ item: any; deleted: boolean }>
Form Validation with Conditional Types
1type ValidationRule<T> = {
2 required?: boolean;
3 validator?: (value: T) => boolean;
4 message?: string;
5};
6
7type FormSchema<T> = {
8 [K in keyof T]: ValidationRule<T[K]>;
9};
10
11type ValidationResult<T> = {
12 [K in keyof T]: T[K] extends { required: true }
13 ? string | null
14 : string | null | undefined;
15};
16
17function validateForm<T extends Record<string, any>>(
18 data: T,
19 schema: FormSchema<T>
20): ValidationResult<T> {
21 // Implementation
22 const result = {} as ValidationResult<T>;
23
24 for (const key in schema) {
25 const rule = schema[key];
26 const value = data[key];
27
28 if (rule.required && !value) {
29 result[key] = rule.message || (`${key} is required` as any);
30 } else if (rule.validator && !rule.validator(value)) {
31 result[key] = rule.message || (`${key} is invalid` as any);
32 } else {
33 result[key] = null as any;
34 }
35 }
36
37 return result;
38}
Real-World Application: Type-Safe API Client
Let’s combine these patterns to build a sophisticated, type-safe API client:
1// Base types
2type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
3type APIEndpoint = `/${string}`;
4
5// Route configuration
6interface RouteConfig<
7 TMethod extends HTTPMethod,
8 TPath extends APIEndpoint,
9 TParams = unknown,
10 TResponse = unknown,
11> {
12 method: TMethod;
13 path: TPath;
14 params?: TParams;
15 response: TResponse;
16}
17
18// Extract parameters from path
19type ExtractParams<T extends string> =
20 T extends `${string}:${infer Param}/${infer Rest}`
21 ? { [K in Param]: string } & ExtractParams<Rest>
22 : T extends `${string}:${infer Param}`
23 ? { [K in Param]: string }
24 : {};
25
26// API client
27class TypeSafeAPIClient<
28 TRoutes extends Record<string, RouteConfig<any, any, any, any>>,
29> {
30 constructor(private routes: TRoutes) {}
31
32 async call<TRouteName extends keyof TRoutes>(
33 routeName: TRouteName,
34 ...[params]: TRoutes[TRouteName]['params'] extends undefined
35 ? []
36 : [TRoutes[TRouteName]['params']]
37 ): Promise<TRoutes[TRouteName]['response']> {
38 const route = this.routes[routeName];
39
40 // Build URL with parameters
41 let url = route.path;
42 if (params) {
43 Object.entries(params as Record<string, any>).forEach(([key, value]) => {
44 url = url.replace(`:${key}`, String(value)) as APIEndpoint;
45 });
46 }
47
48 // Make request
49 const response = await fetch(url, {
50 method: route.method,
51 body: route.method !== 'GET' ? JSON.stringify(params) : undefined,
52 });
53
54 return response.json();
55 }
56}
57
58// Usage
59const apiRoutes = {
60 getUser: {
61 method: 'GET' as const,
62 path: '/users/:userId' as const,
63 params: {} as { userId: string },
64 response: {} as { id: string; name: string; email: string },
65 },
66 createPost: {
67 method: 'POST' as const,
68 path: '/posts' as const,
69 params: {} as { title: string; content: string },
70 response: {} as { id: string; title: string },
71 },
72} satisfies Record<string, RouteConfig<any, any, any, any>>;
73
74const apiClient = new TypeSafeAPIClient(apiRoutes);
75
76// Type-safe API calls
77const user = await apiClient.call('getUser', { userId: '123' });
78// user is typed as { id: string; name: string; email: string }
79
80const post = await apiClient.call('createPost', {
81 title: 'Hello',
82 content: 'World',
83});
84// post is typed as { id: string; title: string }
Performance Considerations
Advanced TypeScript patterns can impact compilation performance:
Best Practices for Performance
- Limit recursion depth: TypeScript has limits on recursive type instantiation
- Use type aliases: Cache complex types to avoid recomputation
- Prefer interfaces over types: For object shapes, interfaces are more performant
- Use conditional types sparingly: Complex conditional types can slow compilation
1// Good: Simple and cached
2type CachedUserType = User & { permissions: Permission[] };
3
4// Avoid: Complex nested conditionals
5type ComplexType<T> = T extends A
6 ? T extends B
7 ? T extends C
8 ? D
9 : E
10 : F
11 : G;
Conclusion
Advanced TypeScript patterns unlock incredible power for building robust, maintainable applications. These patterns help you:
- Catch bugs at compile time instead of runtime
- Improve developer experience with better autocomplete and refactoring
- Express complex business logic in the type system
- Build more maintainable codebases with self-documenting types
Start incorporating these patterns gradually. Begin with branded types and mapped types, then progress to conditional and template literal types as you become more comfortable.
🎯 Next Steps: Try implementing a type-safe state machine using these patterns. Combine conditional types with template literals to create a fully typed workflow system.
The investment in learning advanced TypeScript patterns pays dividends in code quality, developer productivity, and bug prevention. Your future self (and your teammates) will thank you!
What advanced TypeScript patterns have you found most useful in your projects? Share your experiences and examples in the comments!