EdgeCases Logo
Dec 2025
TypeScript
Expert
6 min read

TypeScript Performance: Recursive Types & Build Times

Deeply nested recursive types crash the compiler—learn to optimize with interface caching and tail recursion

typescript
performance
compiler
recursive-types
optimization
edge-case

The Infinite Recursion Trap

TypeScript's type system is Turing complete, which means you can write types that never terminate. While useful for complex logic like JSON parsers or DeepPartial<T>, recursive types are the #1 cause of slow builds and editor lag in large projects.

When the compiler encounters a recursive type, it has to instantiate it. If that instantiation triggers another instantiation, you enter a loop. TypeScript has a hard limit (usually around 50-100 levels) before it throws Type instantiation is excessively deep and possibly infinite. But even before hitting that limit, you're burning CPU cycles.

The Problem: Exponential Instantiation

Consider a naive DeepPartial implementation. It looks innocent, but applying it to a complex nested object forces the compiler to traverse every single property subtree immediately.

// ❌ Naive implementation: Heavy recursive load
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

interface LargeState {
  users: User[];
  settings: Settings;
  // ... dozens of other complex fields
}

// 💥 Triggers massive type instantiation graph
type PartialState = DeepPartial<LargeState>;

Optimization Strategies

1. Interface Caching (Named Types)

The compiler caches named types (interfaces) better than anonymous type aliases. Breaking a large recursive type into smaller, named interfaces can significantly reduce type checking time by allowing the compiler to reuse work.

// ✅ Better: Break recursion with interfaces
interface UserPartial {
  name?: string;
  role?: string;
}

interface StatePartial {
  users?: UserPartial[]; // Uses cached interface
  // ...
}

2. Tail-Recursion Optimization

Just like runtime code, TypeScript types can be optimized for tail recursion. By passing the accumulator as a generic argument, you prevent the compiler from building a massive stack of deferred operations.

// ❌ Stack heavy
type Reverse<T extends any[]> = 
  T extends [infer Head, ...infer Tail] 
    ? [...Reverse<Tail>, Head] 
    : [];

// ✅ Tail recursive (Accumulator pattern)
type ReverseOpt<T extends any[], Acc extends any[] = []> = 
  T extends [infer Head, ...infer Tail]
    ? ReverseOpt<Tail, [Head, ...Acc]>
    : Acc;

From TypeScript 4.5+, the compiler specifically detects and optimizes tail-recursive conditional types, allowing for much deeper recursion limit (up to 1000s of iterations) without crashing.

3. Defer Resolution with mapped types

You can sometimes trick the compiler into deferring resolution until access by wrapping the recursion in an object type.

// ⚡️ Defer resolution trick
type Defer<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;

Advertisement

Advertisement