DeveloperBootstrap

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 UserId

Decorators

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}