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.
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.
The TypeScript error message will point to a specific line. Look at the type definition:
// ❌ 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
Instead of one large conditional, split it into smaller, named helpers:
// ✅ 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.
TypeScript defers instantiation of types in object/array properties. Use this to avoid hitting recursion limits:
// ❌ 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.
Distributive conditional types iterate over each union member, causing exponential growth:
// ❌ 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.
For recursive types, add a depth counter to prevent infinite recursion:
// ❌ 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;Character-by-character string processing is expensive. Limit string lengths:
// ❌ 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.
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
npm install --save-dev typescript@latestIf you're on an older version, upgrading may automatically resolve the issue.
After refactoring:
1. Run TypeScript type checking:
npx tsc --noEmit2. 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:
import type { YourType } from './types';
// Should show inferred type without error
const test: YourType<SomeInput> = /* ... */;### 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:
// 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:
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
Function expression requires a return type
Function expression requires a return type
Value of type 'string | undefined' is not iterable
How to fix "Value is not iterable" in TypeScript
Type 'undefined' is not assignable to type 'string'
How to fix "Type undefined is not assignable to type string" in TypeScript
Type narrowing from typeof check produces 'never'
How to fix "Type narrowing produces never" in TypeScript
Type parameter 'T' has conflicting constraints
How to fix "Type parameter has conflicting constraints" in TypeScript