DNA🔷 TypeScriptAdvanced TypeScript Patterns
ðŸĶ–DinosaurTypeScriptAdvancedType System

Advanced TypeScript Patterns

Generics, conditional types, mapped types, and template literals are the tools that separate TypeScript users from TypeScript architects.

Advanced TypeScript Patterns

Knowing interface vs type is table stakes. Senior frontend interviews test whether you can model complex domains with the type system — and whether you know when not to.

Generics: Parameterized Types

Generics let you write functions and types that work with any type while preserving type information:

function first<T>(arr: T[]): T | undefined {
  return arr[0];
}
 
first([1, 2, 3]);       // number
first(['a', 'b', 'c']); // string

Constrained Generics

Restrict what types are accepted with extends:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
 
const user = { name: 'Alex', age: 30 };
getProperty(user, 'name'); // string
getProperty(user, 'foo');  // Error: '"foo"' is not assignable to keyof typeof user

Generic Components in React

interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => ReactNode;
  keyExtractor: (item: T) => string;
}
 
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return <ul>{items.map((item) => <li key={keyExtractor(item)}>{renderItem(item)}</li>)}</ul>;
}
 
// Usage: T is inferred as User
<List items={users} renderItem={(u) => u.name} keyExtractor={(u) => u.id} />

Conditional Types

Types that depend on a condition — TypeScript's ternary operator at the type level:

type IsString<T> = T extends string ? true : false;
 
type A = IsString<'hello'>; // true
type B = IsString<42>;      // false

Distributive Conditional Types

When T is a union, conditional types distribute across each member:

type Extract<T, U> = T extends U ? T : never;
 
type Numbers = Extract<string | number | boolean, number>;
// number — only the member that extends number survives

infer: Pattern Matching for Types

Extract types from complex structures:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type PromiseValue<T> = T extends Promise<infer V> ? V : T;
 
type A = ReturnType<() => string>;         // string
type B = PromiseValue<Promise<number>>;     // number
type C = PromiseValue<string>;              // string (passthrough)

Mapped Types

Transform existing types by iterating over their keys:

type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Optional<T> = { [K in keyof T]?: T[K] };
type Nullable<T> = { [K in keyof T]: T[K] | null };

Key Remapping (TypeScript 4.1+)

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
 
type UserGetters = Getters<{ name: string; age: number }>;
// { getName: () => string; getAge: () => number }

Filtering Keys

type PickByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K];
};
 
type StringProps = PickByType<{ name: string; age: number; active: boolean }, string>;
// { name: string }

Template Literal Types

String manipulation at the type level:

type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<'click'>; // 'onClick'
 
type CSSProperty = 'margin' | 'padding';
type CSSDirection = 'top' | 'right' | 'bottom' | 'left';
type CSSRule = `${CSSProperty}-${CSSDirection}`;
// 'margin-top' | 'margin-right' | ... | 'padding-left' (8 combinations)

Practical: Type-Safe Event Emitter

type EventMap = {
  click: { x: number; y: number };
  keydown: { key: string };
};
 
type EventHandler<T extends keyof EventMap> = (payload: EventMap[T]) => void;
 
class Emitter<E extends Record<string, any>> {
  private handlers = new Map<string, Function[]>();
 
  on<K extends keyof E & string>(event: K, handler: (payload: E[K]) => void) {
    const list = this.handlers.get(event) ?? [];
    list.push(handler);
    this.handlers.set(event, list);
  }
 
  emit<K extends keyof E & string>(event: K, payload: E[K]) {
    this.handlers.get(event)?.forEach((fn) => fn(payload));
  }
}
 
const bus = new Emitter<EventMap>();
bus.on('click', ({ x, y }) => {}); // fully typed
bus.emit('keydown', { key: 'Enter' }); // fully typed

Discriminated Unions

The single most useful pattern for modeling state:

type AsyncState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };
 
function render(state: AsyncState<User>) {
  switch (state.status) {
    case 'idle': return null;
    case 'loading': return <Spinner />;
    case 'success': return <Profile user={state.data} />; // data is narrowed to User
    case 'error': return <ErrorMessage error={state.error} />;
  }
}

The status field is the discriminant — TypeScript narrows the union based on its value, making illegal states unrepresentable.

Interview Signal

Advanced TypeScript questions test:

  1. Modeling skill — can you represent business rules as types that prevent invalid states?
  2. Utility type fluency — knowing Pick, Omit, Extract, Exclude, ReturnType, and when to compose them
  3. Restraint — knowing when a simple Record or union is better than a 10-line conditional mapped generic type
  4. Practical application — generics in React components, discriminated unions for state machines, template literals for API contracts