Repository navigation
Tail recursion optimization limit 999 may be manipulated #49459
Description
Activity
- addedBugA bug in TypeScriptA bug in TypeScriptHelp WantedYou can do thisYou can do this
on Jun 9, 2022 Is this really a bug?
1000is right on the edge of the recursion limit for tail-call optimizations, so I'm not surprised that two slightly different formulations would have two different behaviors near that limit. If you use something like500instead you'll see that both actually do use tail-call optimization, as opposed to something liketype NumberToTuple3<N, T extends 0[] = []> = T['length'] extends N ? T : (NumberToTuple3<N, [...T, 0]> & {});
which doesn't:
type BigT_Computed = NumberToTuple1<500> // ok type BigT_StillOkay = NumberToTuple2<500> // ok type BigT_Bad = NumberToTuple3<500> // ☠
I admit I'd think
NumberToTuple1would bomb out beforeNumberToTuple2but I don't think that changes whether or not they are using tail-call detection.(Also, you confused me for quite a bit by shadowing the
Numberinterface with a type parameter namedNumber. I'd stick withNandTor evenNumandTupleunless you have some compelling reason to makeX extends Numbermean something other than what it normally means)RyanCavanaugh commented
on Jun 9, 2022 MemberMore actionsIs this really a bug?
It's a bug in the sense that if someone can and will fix it, I'm not going to stop them. That's about it though.
Joe Calzaretta (@jcalz) thank you for the comment, I've renamed
Number- you are correct, it was a bad idea for a var name. Also, I agree with you that tail call optimization is getting applied in both cases, just differently:NumberToTuple2is working well up to 999, while non-optimized recursion is failing at about 45. On the other hand,NumberToTuple1is working up to 3153. I agree that we can consider it more as a hack then as a bug.Ryan Cavanaugh (@RyanCavanaugh) thank you for taking a look! Not sure that there is something can be fixed but ensuring that the limit for both types would be the same (probably 999?)
A funny thing to note is that adding more levels of nasting would get the limit back to 999
type NumberToTuple3<Num extends number, Tuple extends 0[] = []> = 0 extends 1 ? never : 0 extends 1 ? never : Tuple['length'] extends Num ? Tuple : NumberToTuple2<Num, [...Tuple, 0]>; type StillWorking = NumberToTuple3<999> type AlreadyAnError = NumberToTuple3<1000>
- changed the title
[-]Tail recursion optimization detection bug[/-][+]Tail recursion optimization limit 999 may be manipulated[/+]on Jun 9, 2022 Any conclusions?
I faced a similar issue and I've investigated the compiler for a moment. Here I left what I've found for people who might wonder what's going on here.
I was looking for tricks to bypass recursion depth limit, eventually I've arrived to this magical type:
type Magic<T> = T
Wrapping any tail recursive conditional type with this type allows it bypass iteration limit of 1000.
type NumberToTuple3<Num extends number, Tuple extends 0[] = []> = Magic<Tuple['length'] extends Num ? Tuple : NumberToTuple3<Num, [...Tuple, 0]>>; // It's okay to duplicate `Magic` type NumberToTuple4<Num extends number, Tuple extends 0[] = []> = Magic<Magic<Tuple['length'] extends Num ? Tuple : NumberToTuple3<Num, [...Tuple, 0]>>>;
Surprised, I looked over the compiler, and found a function called
canTailRecurseinchecker.ts. It was for checking whether the given type is valid to perform tail call optimization. There was a one and only point of updating the tail recursion counter:if (newRoot.aliasSymbol) { tailCount++; }
This conditional statement was left with following comment:
Note that recursion is possible only through aliased conditional types, so we only increment the tail recursion counter for those.
It seems the author expected every recursive conditional type to have
aliasSymbol. But somehow there were exceptions. I've examined example blow, and found that some recursive conditional types (including ones wrapped withMagic) doesn't havealiasSymbolin their representation.type Magic<T> = T type NTuple<N extends number, T, R extends unknown[] = []> = Magic<R['length'] extends N ? R : NTuple<N, T, [T, ...R]>> type Inc<N extends number> = [0, ...NTuple<N, 0>]['length'] type Test = Inc<1003>
I don't know how
aliasSymbolis constructed and maintained, but I think it's the one who makes this problem.P.S this bug seems to exist from the beginning of TCO for recursive conditional types. (#45711)
Reacted by Sergei SolovevReacted by JongChan Choi (Rieul), devcaeg and Jonathan MASSUCHETTI- addedDomain: Conditional TypesThe issue relates to conditional typesThe issue relates to conditional types
on Oct 16, 2025
Bug Report
🔎 Search Terms
tail recursion, tail recursion optimization, tail recursion detection, tail recursion limit
🕗 Version & Regression Information
⏯ Playground Link
Playground link with relevant code
💻 Code
🙁 Actual behavior
NumberToTuple1is using the tail recursion optimization soBigTuple_Computedtype is[0, 0, 0, ..., 0], just as expected.NumberToTuple2isNOT using tail recursion optimizationusing tail recursion optimization in a different maneer soBigTuple_Errortype isany.The difference between them is only in junk piece of code
0 extends 1 ? never :that is doing nothing.UPD:
NumberToTuple2is working up to 999, whileNumberToTuple1is working up to 3153🙂 Expected behavior
According to the article introducing tail call optimization to TypeScript, tail call optimization should be applied to bothRecursion limit is expected to remain the sameNumberToTuple1andNumberToTuple2the same, because non of them is doing any on-stack transformations with the call results.