Advanced Generics

Hard

Advanced generic patterns go beyond simple type parameters. They include recursive types, higher-order generics (generics that take other generics), variadic tuple types, and builder patterns. These patterns power the type systems of popular libraries like Zod, tRPC, and Prisma, and understanding them separates intermediate TypeScript developers from advanced ones.

Interactive Visualization

Key Points

  • Recursive generic types can reference themselves for tree-like or nested structures
  • Variadic tuple types (`...T`) let generics work with tuples of arbitrary length
  • Higher-order generics accept other generic types as parameters
  • The builder pattern in TypeScript uses method chaining with generic return types
  • Template literal types combined with generics enable string pattern matching
  • Distributive conditional types within generics create powerful type-level logic
  • Const type parameters (`<const T>`) preserve literal types without `as const`

Code Examples

Recursive Types

// JSON type — recursively defines valid JSON values
type JsonValue =
  | string
  | number
  | boolean
  | null
  | JsonValue[]
  | { [key: string]: JsonValue }

// Deep readonly — makes all nested properties readonly
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? T[K] extends Function
      ? T[K]
      : DeepReadonly<T[K]>
    : T[K]
}

interface Config {
  db: { host: string; port: number }
  features: { darkMode: boolean }
}

type FrozenConfig = DeepReadonly<Config>
// db.host is readonly, features.darkMode is readonly, etc.

// Deeply nested path type
type Path<T, Prefix extends string = ''> = {
  [K in keyof T & string]: T[K] extends object
    ? Path<T[K], `${Prefix}${K}.`>
    : `${Prefix}${K}`
}[keyof T & string]

Recursive types reference themselves in their definition. They are essential for modeling tree structures, deeply nested objects, and JSON-like data.

Variadic Tuple Types

// Concat two tuples
type Concat<A extends unknown[], B extends unknown[]> = [...A, ...B]

type AB = Concat<[1, 2], [3, 4]>  // [1, 2, 3, 4]

// Type-safe curry function
type Curry<Args extends unknown[], Return> =
  Args extends [infer First, ...infer Rest]
    ? (arg: First) => Curry<Rest, Return>
    : Return

type CurriedAdd = Curry<[number, number, number], number>
// (arg: number) => (arg: number) => (arg: number) => number

// Head and Tail of a tuple
type Head<T extends unknown[]> = T extends [infer H, ...unknown[]] ? H : never
type Tail<T extends unknown[]> = T extends [unknown, ...infer R] ? R : never

type H = Head<[1, 2, 3]>  // 1
type T = Tail<[1, 2, 3]>  // [2, 3]

// Zip two tuples
type Zip<A extends unknown[], B extends unknown[]> =
  A extends [infer AH, ...infer AT]
    ? B extends [infer BH, ...infer BT]
      ? [[AH, BH], ...Zip<AT, BT>]
      : []
    : []

Variadic tuple types use the spread operator at the type level to manipulate tuples of arbitrary length. They enable type-safe implementations of functional programming patterns.

Builder Pattern with Generics

// Type-safe query builder
class QueryBuilder<
  Selected extends string = never,
  Filtered extends string = never,
> {
  private selectCols: string[] = []
  private whereClauses: string[] = []

  select<C extends string>(
    ...columns: C[]
  ): QueryBuilder<Selected | C, Filtered> {
    this.selectCols.push(...columns)
    return this as unknown as QueryBuilder<Selected | C, Filtered>
  }

  where<C extends string>(
    column: C,
    value: unknown
  ): QueryBuilder<Selected, Filtered | C> {
    this.whereClauses.push(`${column} = ?`)
    return this as unknown as QueryBuilder<Selected, Filtered | C>
  }

  build(): { select: Selected; where: Filtered } {
    return { select: '' as Selected, where: '' as Filtered }
  }
}

const query = new QueryBuilder()
  .select('name', 'email')
  .where('age', 30)
  .build()
// type: { select: 'name' | 'email'; where: 'age' }

The builder pattern accumulates type information through method chaining. Each method call extends the generic parameters, building up a complete type description incrementally.

Common Mistakes

  • Creating infinitely recursive types without a base case, causing compiler hangs
  • Over-engineering types that could be simpler — complexity should be justified
  • Not testing complex generic types with various inputs to verify correctness
  • Forgetting that TypeScript has a recursion depth limit (~50 levels) for type evaluation
  • Using variadic tuples when a simpler array type would suffice

Interview Tips

  • Know how to build DeepReadonly and DeepPartial from scratch
  • Variadic tuple types show advanced knowledge — mention them when discussing function typing
  • The builder pattern demonstrates practical advanced generics in real applications
  • Be honest about complexity — acknowledge when simpler alternatives exist

Related Concepts