Resources / Blog / TypeScript Mastery

Advanced TypeScript Patterns: From Intermediate to Expert Level

Master advanced TypeScript concepts including conditional types, mapped types, type guards, and practical patterns used in large-scale applications.

Jan 27, 202612 min readMaria Rodriguez
TypeScriptType SystemAdvanced PatternsGenericsUtility TypesType Safety

Understanding TypeScript's Type System

TypeScript's true power lies in its sophisticated type system that goes far beyond simple type annotations. Advanced patterns enable developers to create type-safe, self-documenting, and maintainable codebases.

Conditional Types

Conditional types allow types to be selected based on conditions, similar to ternary operators in JavaScript but at the type level.

type IsString<T> = T extends string ? true : false;

type ExtractReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

// Practical example: Filter array types
type FilterArray<T> = T extends any[] ? T : never;

type StringArray = FilterArray<string[]>; // string[]
type NotArray = FilterArray<string>; // never

// Nested conditional types
type DeepNonNullable<T> = {
  [K in keyof T]: T[K] extends object
    ? DeepNonNullable<T[K]>
    : NonNullable<T[K]>;
};

Mapped Types and Key Remapping

Mapped types transform existing types by iterating over their keys, enabling powerful type transformations and constraints.

// Basic mapped type
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

// Conditional mapped type
type OptionalIfUndefined<T> = {
  [K in keyof T]: undefined extends T[K] ? T[K] | undefined : T[K];
};

// Key remapping with as clause (TypeScript 4.1+)
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

// Filter keys by value type
type StringKeys<T> = {
  [K in keyof T]: T[K] extends string ? K : never;
}[keyof T];

Template Literal Types

Template literal types combine string literal types to create new string literal types, enabling powerful string manipulation at the type level.

type EventName = 'click' | 'hover' | 'focus';
type HandlerName = `on${Capitalize<EventName>}`;

// Result: 'onClick' | 'onHover' | 'onFocus'

// Advanced pattern: Path generation
type Paths<T, Prefix extends string = ''> = {
  [K in keyof T]: T[K] extends object
    ? `${Prefix}${string & K}.${Paths<T[K], ''>}`
    : `${Prefix}${string & K}`;
}[keyof T];

// Example usage
type User = {
  name: string;
  address: {
    street: string;
    city: string;
  };
};

type UserPaths = Paths<User>; // 'name' | 'address.street' | 'address.city'

Advanced Generic Constraints

Sophisticated generic constraints enable type-safe APIs and utilities that maintain flexibility while providing compile-time safety.

// Constrain to object keys
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

// Multiple constraints
function mergeObjects<T extends object, U extends object>(
  obj1: T,
  obj2: U
): T & U {
  return { ...obj1, ...obj2 };
}

// Conditional generic constraints
type EnsureArray<T> = T extends any[] ? T : T[];

// Recursive generic constraints
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

Type Guards and Assertion Functions

Advanced type narrowing techniques that provide runtime type safety while maintaining TypeScript's compile-time guarantees.

// Custom type guard
function isStringArray(value: unknown): value is string[] {
  return Array.isArray(value) && value.every(item => typeof item === 'string');
}

// Assertion function
function assertIsNumber(
  value: unknown,
  message?: string
): asserts value is number {
  if (typeof value !== 'number') {
    throw new Error(message || 'Value must be a number');
  }
}

// Discriminated union type guard
type SuccessResponse = { status: 'success'; data: unknown };
type ErrorResponse = { status: 'error'; message: string };
type ApiResponse = SuccessResponse | ErrorResponse;

function handleResponse(response: ApiResponse) {
  if (response.status === 'success') {
    // TypeScript knows response.data exists here
    console.log(response.data);
  } else {
    // TypeScript knows response.message exists here
    console.error(response.message);
  }
}

Utility Type Implementations

Understanding and implementing TypeScript's built-in utility types provides deep insight into the type system.

// Implement Pick
type MyPick<T, K extends keyof T> = {
  [P in K]: T[P];
};

// Implement Omit
type MyOmit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

// Implement Partial
type MyPartial<T> = {
  [P in keyof T]?: T[P];
};

// Implement Required
type MyRequired<T> = {
  [P in keyof T]-?: T[P];
};

// Implement Record
type MyRecord<K extends keyof any, T> = {
  [P in K]: T;
};

Real-World Application Patterns

These patterns combine multiple advanced concepts to solve real-world problems in type-safe ways.

// Type-safe event emitter
type EventMap = {
  click: { x: number; y: number };
  hover: { element: HTMLElement };
  submit: { data: FormData };
};

class EventEmitter<T extends Record<string, any>> {
  private listeners: Map<keyof T, Array<(data: T[keyof T]) => void>> = new Map();

  on<K extends keyof T>(event: K, listener: (data: T[K]) => void) {
    const eventListeners = this.listeners.get(event) || [];
    eventListeners.push(listener as any);
    this.listeners.set(event, eventListeners);
  }

  emit<K extends keyof T>(event: K, data: T[K]) {
    const eventListeners = this.listeners.get(event) || [];
    eventListeners.forEach(listener => listener(data));
  }
}

// Builder pattern with type safety
interface QueryBuilder<T> {
  select<K extends keyof T>(...fields: K[]): QueryBuilder<Pick<T, K>>;
  where<K extends keyof T>(field: K, value: T[K]): QueryBuilder<T>;
  build(): string;
}

// Factory function with discriminated union
type ComponentConfig =
  | { type: 'button'; label: string; onClick: () => void }
  | { type: 'input'; placeholder: string; onChange: (value: string) => void };

function createComponent(config: ComponentConfig): HTMLElement {
  switch (config.type) {
    case 'button':
      // config is narrowed to button config
      const button = document.createElement('button');
      button.textContent = config.label;
      button.addEventListener('click', config.onClick);
      return button;
    case 'input':
      // config is narrowed to input config
      const input = document.createElement('input');
      input.placeholder = config.placeholder;
      input.addEventListener('input', e => config.onChange((e.target as HTMLInputElement).value));
      return input;
  }
}

Best Practices and Pitfalls

  • Avoid excessive type complexity - keep types readable
  • Use type aliases for complex types to improve readability
  • Leverage TypeScript's built-in utility types when possible
  • Test complex types with type-level unit tests
  • Document complex type transformations with comments
  • Use satisfies operator for type assertions that preserve literal types
  • Avoid any and unknown without proper type guards
  • Use const assertions for literal type preservation