TypeScript Generics Deep Dive: Advanced Patterns That Eliminate Entire Bug Classes

TypeScript Generics Deep Dive: Advanced Patterns That Eliminate Entire Bug Classes

Most TypeScript developers use generics as a slightly smarter version of any. The real power — conditional types, mapped types, template literal types, the infer keyword — goes almost entirely unused. This post covers the patterns that actually eliminate bugs.

TL;DR: Conditional types (T extends U ? X : Y) let you compute types at compile time. infer extracts types from other types. Mapped types transform object shapes. Template literal types construct string types programmatically. Together they make illegal states unrepresentable.

Conditional Types: Type-Level if/else

// Basic conditional type
type IsString = T extends string ? true : false;
type A = IsString;  // true
type B = IsString;  // false

// Extract return type of any function
type ReturnType = T extends (...args: any[]) => infer R ? R : never;
type Fn = () => { id: number; name: string };
type FnReturn = ReturnType; // { id: number; name: string }

// Distributive conditional types
type ToArray = T extends any ? T[] : never;
type StringOrNumArray = ToArray;
// Result: string[] | number[]  NOT (string | number)[]

The infer Keyword: Extracting Types from Types

// Extract first argument type
type FirstArg = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type Fn1 = (name: string, age: number) => void;
type Name = FirstArg; // string

// Extract Promise resolved type
type Resolve = T extends Promise ? R : T;
type Resolved = Resolve>; // { id: number }

// Extract array element type
type ElementType = T extends (infer E)[] ? E : never;
type StrEl = ElementType; // string

Mapped Types: Transforming Object Shapes

// Remap keys with as clause (TS 4.1+)
type Getters = {
  [K in keyof T as `get${Capitalize}`]: () => T[K]
};
type User = { name: string; age: number };
type UserGetters = Getters;
// { getName: () => string; getAge: () => number }

// Filter object keys by value type
type PickByValue = {
  [K in keyof T as T[K] extends V ? K : never]: T[K]
};
type OnlyStrings = PickByValue<{ a: string; b: number; c: string }, string>;
// { a: string; c: string }

// Remove readonly
type Mutable = { -readonly [K in keyof T]: T[K] };

// Remove optional
type Required = { [K in keyof T]-?: T[K] };

Template Literal Types: String Type Algebra

// Build string types programmatically
type EventName = `on${Capitalize}`;
type ClickEvent = EventName<'click'>;   // 'onClick'

// Generate all combinations
type Direction = 'top' | 'bottom' | 'left' | 'right';
type Padding = `padding${Capitalize}`;
// 'paddingTop' | 'paddingBottom' | 'paddingLeft' | 'paddingRight'

// Type-safe event emitter
type EventMap = { click: MouseEvent; keydown: KeyboardEvent; focus: FocusEvent };
type EventKey = keyof EventMap;

class TypedEmitter {
  on(event: K, handler: (e: EventMap[K]) => void): void {}
  emit(event: K, data: EventMap[K]): void {}
}
const emitter = new TypedEmitter();
emitter.on('click', (e) => e.clientX); // e is MouseEvent

Making Impossible States Unrepresentable

// Bad: allows impossible combinations
type FetchState = {
  isLoading: boolean;
  data: User | null;
  error: Error | null;
};

// Good: discriminated union
type FetchState =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: User }
  | { status: 'error'; error: Error };

function render(state: FetchState) {
  switch (state.status) {
    case 'success': return state.data; // data is User, not User|null
    case 'error':   return state.error.message; // error is Error
  }
}

// Exhaustiveness check
function assertNever(x: never): never {
  throw new Error('Unexpected: ' + x);
}
// default: assertNever(state) errors if you miss a union member

TypeScript Generics Cheat Sheet

  • ✅ Conditional types: T extends U ? X : Y for type-level logic
  • infer to extract types from complex type patterns
  • ✅ Mapped types with as clause to remap keys
  • ✅ Template literal types for string type algebra
  • ✅ Discriminated unions to make impossible states unrepresentable
  • assertNever for exhaustive switch checking
  • ❌ Never use as casts — fix the types instead
  • ❌ Never reach for any — use unknown and narrow it

TypeScript generics pair naturally with the V8 JIT optimization guide — TypeScript’s type narrowing produces monomorphic code that V8 compiles fastest. See the generator functions guide for TypeScript async generator typing patterns. External reference: TypeScript handbook on types from types.

Master TypeScript advanced type patterns

View Course on Udemy — Hands-on video course covering every concept in this post and more.

Sponsored link. We may earn a commission at no extra cost to you.


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply