How to fix Conditional type branch produces a union type that is too complex to represent in TypeScript

TypeScriptADVANCEDMEDIUMUpdated Dec 8, 2025· 138 views

This error occurs when TypeScript's conditional types generate union branches that exceed the compiler's complexity limits. This typically happens with recursive conditional types or deeply nested generic constraints. Refactoring to break down complex type logic into simpler pieces usually resolves the issue.

Quick fix

Fixes TS2590

// ❌ This might be flagged as too complex
type ExtractFromUnion<T> = T extends { prop: infer U }
  ? U
  : T extends { other: infer V }
  ? V
  : never;

type Result = ExtractFromUnion<
  | { prop: string }
  | { other: number }
  | { prop: boolean }
  // ... many more union members
>;
  1. Identify which type is too complex
  2. Break down the conditional type
  3. Use object properties instead of recursion
  4. Avoid distributive conditionals on large unions
typescript — error
// ❌ This might be flagged as too complex
type ExtractFromUnion<T> = T extends { prop: infer U }
? U
: T extends { other: infer V }
? V
: never;
type Result = ExtractFromUnion<
| { prop: string }
| { other: number }
| { prop: boolean }
// ... many more union members
>;
Conditional type branch produces a union type that is too complex to represent

What this error means

TypeScript's type system has built-in limits to prevent infinite type recursion and ensure reasonable compiler performance. When you use conditional types (the ternary operator for types), especially in recursive or distributive scenarios with unions, TypeScript may hit these internal limits: 1. **Instantiation depth limit**: Tracks how deeply nested type parameters can be instantiated (default: 50, with a maximum tail recursion depth of 1000). 2. **Tail recursion depth limit**: When a conditional type references itself, TypeScript tries to optimize with tail recursion, but has a maximum depth of 1000. 3. **Union explosion**: When distributive conditional types process union members, each union element may spawn multiple type branches, causing exponential growth. The "too complex to represent" error (TS2590) occurs when TypeScript gives up trying to resolve a union that has grown too large or deeply nested. The compiler cannot meaningfully track what the final type should be because the complexity has exceeded its reasoning capability.

Typical symptoms

  • Error appears during type checking with 'too complex to represent' message
  • The error vanishes when you simplify or break apart the conditional type
  • It happens with recursive conditional types that process unions or strings
  • TypeScript IntelliSense becomes slow or times out in related files
  • Works in isolation but fails in larger projects with accumulated type complexity
  • The error points to a type alias or function signature that seems reasonable in isolation

Common causes

  1. Recursive conditional types without clear termination conditions (most common)
  2. Distributive conditional types applied to large unions (each union member doubles the branches)
  3. Deeply nested generic constraints with multiple type parameters
  4. Attempting to extract/split large literal string types character by character
  5. Conditional types that reference themselves transitively through other type aliases
  6. Using conditional types in constraint positions on deeply generic functions
  7. Type utilities that weren't designed with recursive safety in mind (e.g., deep Omit, Pick on large interfaces)

How to fix "Conditional type branch produces a union type that is too complex to represent"

1Identify which type is too complex

The TypeScript error message will point to a specific line. Look at the type definition:

typescript
// ❌ This might be flagged as too complex
type ExtractFromUnion<T> = T extends { prop: infer U }
  ? U
  : T extends { other: infer V }
  ? V
  : never;

type Result = ExtractFromUnion<
  | { prop: string }
  | { other: number }
  | { prop: boolean }
  // ... many more union members
>;

Look for:
- Recursive conditional type definitions
- Deeply nested generics
- Distributive conditionals applied to large unions
- Character-by-character string processing

2Break down the conditional type

Instead of one large conditional, split it into smaller, named helpers:

typescript
// ✅ Better: Simpler intermediate steps
type HasProp<T> = T extends { prop: infer U } ? U : never;
type HasOther<T> = T extends { other: infer V } ? V : never;

type ExtractValue<T> = HasProp<T> | HasOther<T>;

This reduces depth and makes the type easier to reason about.

3Use object properties instead of recursion

TypeScript defers instantiation of types in object/array properties. Use this to avoid hitting recursion limits:

typescript
// ❌ Deep recursion—hits limit
type DeepNest<T, N extends number> = N extends 0
  ? T
  : DeepNest<{ inner: T }, [any, ...any[]]>;

// ✅ Better: Put recursion in a property
type DeepProperty<T, N extends number> = {
  value: N extends 0 ? T : DeepProperty<T, [any, ...any[]]>;
};

Deferring the type instantiation prevents it from counting against your depth limit.

4Avoid distributive conditionals on large unions

Distributive conditional types iterate over each union member, causing exponential growth:

typescript
// ❌ Distributive—one branch per union member × number of conditions
type Extract<T, K extends PropertyKey> = T extends Record<K, any>
  ? T[K]
  : never;

type Result = Extract<
  | { a: string } | { b: number } | { c: boolean } | ...many more
  | Record<string, any>
>;

// ✅ Better: Non-distributive conditional (use square brackets)
type Extract<T, K extends PropertyKey> = [T] extends [Record<K, any>]
  ? T[K]
  : never;

Wrapping in square brackets prevents distribution and keeps complexity linear.

5Limit recursion depth explicitly

For recursive types, add a depth counter to prevent infinite recursion:

typescript
// ❌ Unlimited recursion
type FlattenArray<T> = T extends (infer U)[]
  ? FlattenArray<U>
  : T;

// ✅ Better: Explicit depth limit
type FlattenArray<T, D extends number = 5> = D extends 0
  ? T
  : T extends (infer U)[]
  ? FlattenArray<U, [any, ...any[]] extends [...infer R] ? R['length'] : 0>
  : T;

// Or use a simple numeric count:
type Flatten<T, Depth extends readonly any[] = [1, 2, 3, 4, 5]> = Depth extends readonly [infer _ extends any, ...infer Rest]
  ? T extends (infer U)[]
    ? Flatten<U, Rest>
    : T
  : T;
6Use template literal hacks with caution

Character-by-character string processing is expensive. Limit string lengths:

typescript
// ❌ Dangerous—fails on strings longer than ~20 chars
type Split<S extends string, Acc extends string[] = []> = S extends `${infer First}${infer Rest}`
  ? Split<Rest, [...Acc, First]>
  : Acc;

// ✅ Better: Explicit length limit
type Split<
  S extends string,
  MaxLen extends readonly any[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
  Acc extends string[] = []
> = MaxLen extends readonly [infer _ extends any, ...infer Rest]
  ? S extends `${infer First}${infer Remainder}`
    ? Split<Remainder, Rest, [...Acc, First]>
    : Acc
  : Acc;

Consider using JavaScript at runtime instead of trying to process large strings at type-check time.

7Upgrade TypeScript if possible

Newer TypeScript versions have optimizations:

- TypeScript 4.1+: Tail-recursive conditional types compile in a loop (no call stack)
- TypeScript 4.5+: Performance improvements for complex type operations
- TypeScript 5.0+: Further optimizations and better error messages

bash
npm install --save-dev typescript@latest

If you're on an older version, upgrading may automatically resolve the issue.

8Verify the fix

After refactoring:

1. Run TypeScript type checking:

bash
npx tsc --noEmit

2. Check that your code still compiles without "too complex" errors
3. Verify that IntelliSense works and shows correct types
4. Test that your type utilities produce the expected results:

typescript
import type { YourType } from './types';

// Should show inferred type without error
const test: YourType<SomeInput> = /* ... */;

Advanced notes

### Internal Limits in TypeScript

TypeScript enforces three key limits to prevent runaway compilation:

1. Instantiation Depth: Maximum 50 for general instantiation, 100 for conditional types
2. Tail Recursion Depth: Maximum 1000 when recursion is tail-recursive (recommended optimization)
3. Union Flattening: Unions with more than ~1000 members become problematic

### When to Use Runtime Instead

If your type utility requires:
- Processing strings character-by-character
- Deep tree recursion on large structures
- Complex set operations (union, intersection, difference)

Consider using functions with generics instead of pure type manipulation:

typescript
// Type-level: only validate the shape
type ValidConfig<T> = T extends { key: string } ? T : never;

// Runtime: actual processing
function processConfig<T extends { key: string }>(config: T) {
  // Do expensive operations here
  return config;
}

### Debugging Complex Types

Use type inspect = ... to isolate where complexity explodes:

typescript
type Step1<T> = T extends SomeCondition ? A : B;
type inspect1 = Step1<YourType>; // Check if this is complex

type Step2<T> = Step1<T> extends Another ? C : D;
type inspect2 = Step2<YourType>; // Check if complexity compounds

### References

- TypeScript PR #45025: "Increase type instantiation depth limit" (allows tuning)
- TypeScript PR #45711: "Tail recursive evaluation of conditional types" (optimization)
- Type instantiation is limited to prevent infinite loops and compiler hangs

### Related Tools

- ts-expect-error: Suppress the error temporarily while refactoring
- type-fest: Popular library with well-tested complex type utilities (good examples)
- TypeScript Playground: Test type utilities in isolation: https://www.typescriptlang.org/play

Frequently asked questions

What does "TS2590" mean?
TypeScript's type system has built-in limits to prevent infinite type recursion and ensure reasonable compiler performance. When you use conditional types (the ternary operator for types), especially in recursive or distributive scenarios with unions, TypeScript may hit these internal limits: 1. Instantiation depth limit: Tracks how deeply nested type parameters can be instantiated (default: 50, with a maximum tail recursion depth of 1000).
How do I fix "TS2590"?
This error occurs when TypeScript's conditional types generate union branches that exceed the compiler's complexity limits. This typically happens with recursive conditional types or deeply nested generic constraints. Refactoring to break down complex type logic into simpler pieces usually resolves the issue.
What causes "TS2590"?
Recursive conditional types without clear termination conditions (most common) Distributive conditional types applied to large unions (each union member doubles the branches) Deeply nested generic constraints with multiple type parameters
Is the "TS2590" error serious?
It is a medium-severity error — more of a blocker than a risk. The steps above resolve it.

Official resources & further reading