This TypeScript error occurs when template literal types generate unions that exceed the compiler's complexity limits. Template literal types can create exponential union growth when used with conditional types or string manipulation patterns. Refactoring to use simpler type patterns or limiting string length typically resolves this issue.
TypeScript's template literal types allow you to manipulate and infer string literal types at compile time. However, when template literal types are combined with conditional types or recursive patterns, they can generate unions that grow exponentially in size, hitting TypeScript's internal complexity limits: 1. **Union explosion**: Template literal types that iterate over string characters or combine multiple string literals can create unions with thousands of members. 2. **Distributive behavior**: When template literal types are used in distributive conditional types, each union member spawns its own branch of computation. 3. **Recursive patterns**: Recursive template literal types (like splitting strings character by character) can create deeply nested type computations. The "too complex" error (TS2590) occurs when TypeScript cannot represent the resulting union type because it has grown beyond the compiler's capacity to track and reason about it. This is a protective measure to prevent infinite type recursion and ensure reasonable compilation times.
The error message will point to a specific line. Look for template literal types with inference patterns:
// ❌ This might be flagged as too complex
type Split<S extends string> = S extends `${infer First}${infer Rest}`
? [First, ...Split<Rest>]
: [];
type Result = Split<"abcdefghijklmnopqrstuvwxyz">; // 26-character stringLook for:
- Recursive template literal types
- Template literal types in conditional type branches
- Multiple infer keywords in a single template literal
- Template literal types processing long strings
Add explicit length limits to prevent infinite recursion:
// ✅ Better: Explicit length limit
type Split<
S extends string,
MaxDepth extends readonly any[] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
Acc extends string[] = []
> = MaxDepth extends readonly [infer _ extends any, ...infer Rest]
? S extends `${infer First}${infer Remainder}`
? Split<Remainder, Rest, [...Acc, First]>
: Acc
: Acc;
// Usage with limit
type Result = Split<"hello">; // ["h", "e", "l", "l", "o"]
type LongResult = Split<"abcdefghijklmnopqrstuvwxyz">; // Only processes first 10 charsThis prevents the type from recursing indefinitely.
Instead of processing each character, work with larger chunks:
// ❌ Character-by-character (expensive)
type Reverse<S extends string> = S extends `${infer First}${infer Rest}`
? `${Reverse<Rest>}${First}`
: S;
// ✅ Better: Use built-in string utilities or runtime
type Reverse<S extends string> = string; // Accept any string
// Or use a simpler approach
type LastChar<S extends string> = S extends `${string}${infer Last}` ? Last : S;
type FirstChar<S extends string> = S extends `${infer First}${string}` ? First : S;Consider whether you really need character-level type manipulation.
Wrap template literal types in square brackets to prevent distribution:
// ❌ Distributive - creates union explosion
type ExtractPrefix<T extends string> = T extends `${infer Prefix}-${string}`
? Prefix
: never;
type Result = ExtractPrefix<"foo-bar" | "baz-qux" | "quux-corge">;
// Evaluates to: "foo" | "baz" | "quux" (but can explode with complex unions)
// ✅ Better: Non-distributive
type ExtractPrefix<T extends string> = [T] extends [`${infer Prefix}-${string}`]
? Prefix
: never;This keeps complexity linear instead of exponential.
Split large template literal types into smaller, named helpers:
// ❌ One complex type
type ParseRoute<T extends string> = T extends `/${infer Segment}/${infer Rest}`
? [Segment, ...ParseRoute<`/${Rest}`>]
: T extends `/${infer Segment}`
? [Segment]
: [];
// ✅ Better: Separate helpers
type ExtractSegment<T extends string> = T extends `/${infer Segment}${infer Rest}`
? { segment: Segment; rest: Rest }
: never;
type ParseSegments<T extends string, Acc extends string[] = []> =
ExtractSegment<T> extends { segment: infer S extends string; rest: infer R extends string }
? ParseSegments<R, [...Acc, S]>
: Acc;Smaller types are easier for TypeScript to reason about.
Use template literal types to validate string shapes, not to transform them:
// ✅ Good: Validation only
type IsKebabCase<S extends string> = S extends `${string}-${string}`
? true
: false;
type ValidRoute<S extends string> = S extends `/${string}`
? S
: never;
// Runtime transformation
function toKebabCase(str: string): string {
return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
}Leave complex string manipulation to runtime JavaScript.
Newer TypeScript versions have optimizations for template literal types:
- TypeScript 4.1: Introduced template literal types
- TypeScript 4.3: Performance improvements
- TypeScript 4.4: Better error messages
- TypeScript 4.5+: Further optimizations
npm install --save-dev typescript@latestUpgrading may automatically resolve some complexity issues.
After refactoring:
1. Run TypeScript type checking:
npx tsc --noEmit2. Check that the "too complex" error is gone
3. Verify IntelliSense works correctly
4. Test with various string inputs:
// Should work without errors
type Test1 = Split<"hello">;
type Test2 = ExtractPrefix<"foo-bar">;
type Test3 = IsKebabCase<"my-component">;### TypeScript's Internal Limits
Template literal types hit several internal limits:
1. Union Size Limit: Unions with more than ~1000 members become problematic
2. Instantiation Depth: Maximum 50 for general instantiation
3. Tail Recursion Depth: Maximum 1000 for optimized recursion
### When Template Literal Types Are Appropriate
Use template literal types for:
- Validating string formats (URLs, IDs, routes)
- Simple string manipulation with known bounds
- Type-safe string concatenation
- Branded string types
Avoid template literal types for:
- Character-by-character string processing
- Complex string transformations
- Parsing arbitrary-length strings
- Recursive string algorithms
### Alternative Approaches
1. Runtime validation with type guards:
function isKebabCase(str: string): str is KebabCase {
return /^[a-z]+(-[a-z]+)*$/.test(str);
}2. Branded types:
type KebabCase = string & { __brand: 'kebab-case' };
function toKebabCase(str: string): KebabCase {
return str.replace(/[A-Z]/g, m => '-' + m.toLowerCase()) as KebabCase;
}3. Const assertions:
const routes = ['/home', '/about', '/contact'] as const;
type Route = typeof routes[number]; // '/home' | '/about' | '/contact'### Debugging Template Literal Types
Use intermediate type aliases to see where complexity explodes:
type Step1<S extends string> = S extends `${infer A}${infer B}` ? [A, B] : never;
type inspect1 = Step1<'hello'>; // Check this step
type Step2<S extends string> = Step1<S> extends [infer A, infer B]
? [B, A]
: never;
type inspect2 = Step2<'hello'>; // Check if complexity compounds### References
- TypeScript 4.1 Release Notes: Template Literal Types
- TypeScript Handbook: Template Literal Types
- Type Challenges: Template Literal Type exercises
### Related Errors
- "Conditional type produces union that is too complex"
- "Type instantiation is excessively deep and possibly infinite"
- "Expression produces a union type that is too complex to represent"
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