How to workaround the max recursion depth in TypeScript
Tired of TypeScript whining about “Type instantiation is excessively deep and possibly infinite”? It’s basically the compiler hollering, “Enough with the wizardry—I can only handle so many nested types!” This article reveals a handful of sneaky workarounds, from tail-recursive transformations to unrolling recursion or pulling off a “counter reset” hack. Fair warning: these tricks can make the compiler grind its gears. Sometimes the best solution is to just keep it simple—nobody wants their teammates cursing their name at the daily stand-up.

Have you already seen this error message?
Type instantiation is excessively deep and possibly infinite.
If you're deeply involved with TypeScript 🧙♂️, chances are you've come across this error, causing frustration and leaving you unsure of how to resolve it. Fortunately, I have multiple solutions for you. However, bear in in mind that while these solutions might assist you in gaining a deeper understanding of the problem, they can slow down the compiler. Let's delve into some magic tricks.
The Issue
TypeScript's type system imposes a strict maximum tail recursion depth of 1000, an unchangeable hard limit. When dealing with intricate types, such as attempting complex tasks like solving Advent of Code challenges using only types or developing TypeScript libraries that may lead to intricate type structures, you might encounter this limit.
Consider a Replace
type that substitutes all values in an array with a given value:
type Replace<T extends any[], ReplaceValue> = T extends [any, ...infer Tail]
? [ReplaceValue, ...Replace<Tail, ReplaceValue>]
: [];
A purely head-recursive approach usually forms nested tuples (or nested object structures) at each step. Each level of recursion doesn’t just pass the “problem” along; it also builds up the output structure in a deeply nested way. That means TypeScript must keep track of and expand the entire shape at every step, causing expansions to balloon.
- Every time you recurse, you’re adding
[ReplaceValue, ... (the rest)]
on top. - This leads TypeScript to fully evaluate and represent each expanded tuple type from the outside in.
In practice, you can hit the expansion limit in fewer than 1000 “calls,” because each call can produce a deeply nested type that itself causes further expansions.
The first 🧙♂️-trick
The first solution involves transforming the type to be tail-recursive:
type Replace<T extends any[], ReplaceValue, Agg extends ReplaceValue[] = []> =
T extends [any, ...infer Tail]
? Replace<Tail, ReplaceValue, [...Agg, ReplaceValue]>
: Agg;
In a purely “head-recursive” approach, TypeScript interprets each recursive step as building a new type that depends on the previous one, and the recursion accumulates from the head. By introducing an aggregator Agg
, we switch to tail recursion. The idea is that each recursive call accumulates its result into Agg
and then immediately returns the result of the next call, so TypeScript treats some of that type-building differently—some type systems can optimize this. In TypeScript, it doesn’t always guarantee optimization like a true tail recursion in a compiled language, but it does generally reduce the nesting depth of the final type, letting you handle more array elements (though still limited to around 1000 elements in practice).
If your type still isn’t functioning with this approach, you either need to optimize other types in your code or consider some more advanced “black magic.”
The second 🧙♂️-trick
The initial workaround involves unrolling the loop or recursion. Similar to optimizing loops in languages like C/C++, we can apply a similar technique in TypeScript:
type Replace<T extends any[], ReplaceValue, Agg extends any[] = []> =
T extends [any, any, ...infer Tail]
? Replace<Tail, ReplaceValue, [...Agg, ReplaceValue, ReplaceValue]>
: T["length"] extends 1 ? [...Agg, ReplaceValue] : Agg
By unrolling the recursion like this we can increase the max recursion depth by a factor of 2. So we can now handle arrays with 1998 elements. You can also do 11 or steps at a time then you will run into this new error message. You can work around this aswell but that's another topic.
Type produces a tuple type that is too large to represent.(2799)
Note: This also works with strings.
type Replace<T extends string, ReplaceValue, Agg extends string = ""> =
T extends `${infer _}${infer _}${infer Tail}`
? Replace<Tail, ReplaceValue, `${Agg}${ReplaceValue}${ReplaceValue}`>
: T["length"] extends 1 ? `${Agg}${ReplaceValue}` : Agg;
The third 🧙♂️-trick
Internally, TypeScript keeps track of the recursion limit using a counter. Resetting this counter can be achieved by utilizing an intersection type, effectively resetting the counter to zero. This workaround can be used multiple times, but keep in mind that it will also slow down the compiler:
type Replace<T extends any[], ReplaceValue, Agg extends any[] = [], RecursionCount extends any[] = []> = RecursionCount["length"] extends 500
? Replace<T, ReplaceValue, Agg, []> & {} // reset the counter
: T extends [any, ...infer Tail]
? Replace<Tail, ReplaceValue, [...Agg, ReplaceValue], [...RecursionCount, unknown]>
: Agg;
While not as swift as the first method, this approach extends the limit to approximately >2000 elements before encountering the "Type Instantiation Depth (TS2589)" error. Further information about this method can be found here.
The fourth 🧙♂️-trick
Just don't. Keep in mind that TypeScript is made by devs for devs. It can boost DX, but if you slow tsc down too much, everyone on your project will hate it—and if you publish it on npm and people start using it, they'll end up hating you too. Especially in large codebases—if you’re using tRPC or Zod, you know what I mean—it can drag the whole project down. Most of the time, you just don’t need those fancy tricks. Keep it simple. If you’re only doing it for fun to impress nerds like me, get creative and break this language.
Conclusion
These workarounds offer methods to tackle TypeScript's maximum recursion depth limitation. Whether by unrolling the recursion or resetting the counter, all approaches can aid in understanding the problem better. If you have alternative ideas on surpassing this limitation, I'm eager to hear about them. Feel free to share your thoughts and write me on X.











