TypeScript
TypeScript Advanced Patterns Guide
Master advanced TypeScript patterns including generics, decorators, mapped types, conditional types, and enterprise architectural patterns.
What you'll learn
- Write advanced generic constraints and infer types
- Build utility types with mapped and conditional types
- Implement decorators for cross-cutting concerns
- Use template literal types for type-safe APIs
- Create discriminated unions and exhaustive checks
- Apply branded types for domain modelling
- Write type-safe builder patterns
Advanced Generics
TypeScript
1// Constrained generics with keyof
2function pluck<T, K extends keyof T>(obj: T, keys: K[]): T[K][] {
3 return keys.map(k => obj[k]);
4}
5
6const user = { id: 1, name: 'Alice', email: '[email protected]' };
7const values = pluck(user, ['name', 'email']); // string[]
8
9// Generic with default
10function createStore<T extends object, Initial extends T = T>(
11 initial: Initial
12): { state: T; update: (patch: Partial<T>) => void } {
13 let state = { ...initial };
14 return {
15 state,
16 update: (patch) => { state = { ...state, ...patch }; },
17 };
18}
19
20// Infer — extract the return type of a promise
21type Awaited<T> = T extends Promise<infer U> ? U : T;
22type Result = Awaited<Promise<string>>; // string
23
24// Recursive types
25type DeepReadonly<T> = {
26 readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
27};Mapped Types
TypeScript
1// Make all properties optional and nullable
2type Nullable<T> = { [K in keyof T]: T[K] | null };
3type PartialNullable<T> = { [K in keyof T]?: T[K] | null };
4
5// Filter keys by value type
6type KeysOfType<T, V> = {
7 [K in keyof T]: T[K] extends V ? K : never;
8}[keyof T];
9
10interface User { id: number; name: string; age: number; email: string }
11type StringKeys = KeysOfType<User, string>; // "name" | "email"
12
13// Remap keys with 'as'
14type Getters<T> = {
15 [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
16};
17type UserGetters = Getters<User>;
18// { getId: () => number; getName: () => string; ... }Conditional Types
TypeScript
1// Flatten nested arrays
2type Flatten<T> = T extends Array<infer U> ? U : T;
3type Str = Flatten<string[]>; // string
4type Num = Flatten<number>; // number
5
6// Non-nullable
7type NonNullable<T> = T extends null | undefined ? never : T;
8
9// Extract function parameters
10type Parameters<T extends (...args: never[]) => unknown> =
11 T extends (...args: infer P) => unknown ? P : never;
12
13// Distribute over unions
14type ToArray<T> = T extends unknown ? T[] : never;
15type Result = ToArray<string | number>; // string[] | number[]
16
17// Exclude specific keys from an interface
18type OmitByValue<T, V> = {
19 [K in keyof T as T[K] extends V ? never : K]: T[K];
20};Template Literal Types
TypeScript
1type EventName = 'click' | 'focus' | 'blur';
2type EventHandler = `on${Capitalize<EventName>}`;
3// "onClick" | "onFocus" | "onBlur"
4
5// Type-safe CSS property builder
6type CSSUnit = 'px' | 'rem' | 'em' | '%';
7type CSSValue = `${number}${CSSUnit}`;
8
9function px(value: number): CSSValue {
10 return `${value}px`;
11}
12
13// Type-safe route builder
14type Route = '/users' | '/posts' | '/settings';
15type ApiRoute = `/api${Route}`;
16
17function apiUrl(route: Route): ApiRoute {
18 return `/api${route}`;
19}Discriminated Unions
TypeScript
1type ApiState<T> =
2 | { status: 'idle' }
3 | { status: 'loading' }
4 | { status: 'success'; data: T }
5 | { status: 'error'; error: Error };
6
7// Exhaustive check — TS errors if a case is missing
8function renderState<T>(state: ApiState<T>): string {
9 switch (state.status) {
10 case 'idle': return 'Ready';
11 case 'loading': return 'Loading...';
12 case 'success': return JSON.stringify(state.data);
13 case 'error': return state.error.message;
14 default: {
15 const _exhaustive: never = state;
16 throw new Error(`Unhandled state: ${JSON.stringify(_exhaustive)}`);
17 }
18 }
19}Branded Types
TypeScript
1// Prevent mixing semantically different IDs
2declare const _brand: unique symbol;
3type Brand<T, B> = T & { readonly [_brand]: B };
4
5type UserId = Brand<string, 'UserId'>;
6type CourseId = Brand<string, 'CourseId'>;
7
8function createUserId(id: string): UserId { return id as UserId; }
9function createCourseId(id: string): CourseId { return id as CourseId; }
10
11function getUser(id: UserId): Promise<User> { /* ... */ return Promise.resolve({} as User); }
12
13const userId = createUserId('user-123');
14const courseId = createCourseId('course-456');
15
16getUser(userId); // OK
17getUser(courseId); // TypeScript error — CourseId is not assignable to UserIdDecorators
TypeScript
1// tsconfig.json: "experimentalDecorators": true
2
3// Method decorator — measure execution time
4function measure(target: object, key: string, descriptor: PropertyDescriptor) {
5 const original = descriptor.value as (...args: unknown[]) => unknown;
6 descriptor.value = function (...args: unknown[]) {
7 const start = performance.now();
8 const result = original.apply(this, args);
9 const end = performance.now();
10 console.log(`${key} took ${(end - start).toFixed(2)}ms`);
11 return result;
12 };
13 return descriptor;
14}
15
16class DataService {
17 @measure
18 async fetchUsers(): Promise<User[]> {
19 // expensive DB query
20 return [];
21 }
22}
23
24// Class decorator — singleton
25function singleton<T extends new (...args: unknown[]) => object>(ctor: T): T {
26 let instance: InstanceType<T>;
27 return class extends ctor {
28 constructor(...args: unknown[]) {
29 if (instance) return instance;
30 super(...args);
31 instance = this as InstanceType<T>;
32 }
33 } as T;
34}