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

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

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.

The foundation: generic functions and constraints

Everything later in this guide builds on two ideas. A type parameter such as T is a placeholder the caller fills in, usually by inference, so you write a function once and keep the exact type. A constraint written with extends limits what T can be, so the function body can safely use what the constraint guarantees. Without generics you either duplicate the function per type or fall back to any, which throws away the type information.

typescript
function first<T>(items: readonly T[]): T | undefined {
  return items[0];
}

function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

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

const n = first([1, 2, 3]);                 // number | undefined
const word = longest("hello", "hi");        // string
const list = longest([1, 2, 3], [1, 2]);    // number[]
const user = { name: "Alice", age: 30 };
const age = getProperty(user, "age");       // number
// longest(1, 2);                 // error: number has no 'length'
// getProperty(user, "email");    // error: "email" is not a key of user
console.log(n, word, list, age);

K extends keyof T is the pattern behind type-safe property access: the return type T[K] follows the key you pass, and a misspelled key is a compile error rather than undefined at runtime. All of the above compiles under tsc --strict.

  • Name type parameters by role: T for a single type, K/V for keys and values, E for elements.
  • Constrain only as much as the body needs. Over-constraining makes the function harder to reuse.
  • If a type parameter appears only once in a signature, you probably don’t need it.

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.

Related: TypeScript Generics Deep Dive: Write Reusable Code With Full Type Safety · TypeScript for JavaScript Developers: The Migration Guide That Actually Works · JavaScript Promises: The Complete Guide From Basics to Advanced Patterns

Watch: 100 Days of AI on YouTube

Short videos from CheatCoders, one AI engineering topic per day.

Open the playlist on YouTube · CheatCoders on YouTube

Deep-dive PDF

Get the expanded guide for this post — extra diagrams-style checklists, failure modes, and a production walkthrough. Free when you subscribe to CheatCoders.

Already subscribed? or open the subscribe page.


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 comment

No account needed. Name and email are optional.