-
Notifications
You must be signed in to change notification settings - Fork 12.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Max depth limit does not trigger. Gives up and resolves type to any #32707
Comments
Huh. For the const foo13 = foo12.x(13);
//Type instantiation is excessively deep and possibly infinite.
foo13.arr Same this for the const foo10 = foo9.x(10);
//Type instantiation is excessively deep and possibly infinite.
foo10.arr |
Also, I have an API question. (So, it should be allowed on this issue tracker, right?). Here is an implementation that uses a different generic type parameter, type AppendToArray<ArrT extends any[], T> = (
(
ArrT[number]|
T
)[]
);
class Foo<ArrT extends number[]> {
arr! : ArrT;
x<N extends number> (n:N) : Foo<AppendToArray<ArrT, N>> {
return null!;
}
}
declare const foo0 : Foo<0[]>;
const foo10 = foo0.x(1).x(2).x(3).x(4).x(5).x(6).x(7).x(8).x(9).x(10);
const foo20 = foo10.x(11).x(12).x(13).x(14).x(15).x(16).x(17).x(18).x(19).x(20);
const foo30 = foo20.x(21).x(22).x(23).x(24).x(25).x(26).x(27).x(28).x(29).x(30);
const foo40 = foo30.x(31).x(32).x(33).x(34).x(35).x(36).x(37).x(38).x(39).x(40);
const foo50 = foo40.x(41).x(42).x(43).x(44).x(45).x(46).x(47).x(48).x(49).x(50);
/*
const foo60: Foo<(0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14
| 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30
| 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | ... 17 more ... | 60)[]>
*/
const foo60 = foo50.x(51).x(52).x(53).x(54).x(55).x(56).x(57).x(58).x(59).x(60); As you can see, I can go up to So, my question is, why does TS have such a high limit when the type parameter is just On my project, my real use case, I have to use the non-array object because this object will have a lot of properties and I want them all to be generic. Using one type param per property would be unmanageable. And I'm still surprised my real use case supports 20 consecutive method calls when this |
FYI, in your screenshot above, the type is actually super "simple" from a construction perspective, the printback is just garbage because the object is anonymous but recursive. If you make the object that you're indexing into (assuming it's one of the immediately indexed object patterns) an interface, ie: interface AppendWorker<ArrT extends any[], T> {
0: ...;
1: ...;
}
type Append<ArrT extends any[], T> = AppendWorker<ArrT, T>[...]; you should get a much more sane printback! Edit: Oh, lol, your example is just real deep. Yeah, that's just like 80+ levels of arrays within arrays, lol. |
I intentionally wrote that nonsensical piece of code in the above screenshot to see if it would crash TS. I was very surprised when it did not! I guess it wasn't as "crazy" as I thought. |
So re: the example in the OP - if I had to guess, the error is triggering, but there's no active node to put the error on, since it's triggering during some kind of signature resolution that reset the current node, rather than inside an expression typecheck (or the result was cached in such a situation and then reused for the expression). That's my guess, anyway. It probably indicates a codepath we neglect to set a |
Seems like even going up to I guess TS just doesn't like object type params? |
I just changed that "crazy" example to use an object type param instead of an array type param, type Append<ArrT extends any[], T> = (
(
ArrT[number]|
T
)[]
);
class X<ArrT extends {arr:any[]}> {
t! : ArrT["arr"];
x () : X<{arr:Append<ArrT["arr"], ArrT["arr"]>}> {
return null!;
}
}
declare const x : X<{arr:[]}>;
/*
const okay: X<{
arr: ([] | [][] | ([] | [][])[] | ([] | [][] | ([] | [][])[])[]
| ([] | [][] | ([] | [][])[] | ([] | [][] | ([] | [][])[])[])[]
| ([] | [][] | ([] | [][])[] | ([] | [][] | ([] | [][])[])[]
| ([] | [][] | ([] | [][])[] | ([] | [][] | ([] | [][])[])[])[])[]
| ([] | [][] | ([] | [][])[] | ([] | [][] | ([] | [][])[])[]
| ([] | [][] | ([] | [][])[] | ([] | [][] | ([] | [][])[])[])[]
| ([] | [][] | ([] | [][])[] | ([] | ... 1 more ... | ([] | [][])[])[]
| ([] | ... 2 more ... | ([] | ... 1 more ... | ([] | [][])[])[])[])[])[]
| ... 8 more ... | ([] | ... 13 more ... | ([] | ... 12 more ...
| ([] | ... 11 more ... | ([] | ... 10 more ...
| ([] | ... 9 more ... | ([] | ... 8 more ... | ([] | ... 7 more ...
| ([] | ... 6 more ... | ([] | ... 5 more ... | ([] | ... 4 more ...
| ([] | ... 3 more ... | ([] | ... 2 more ... | ([] | ... 1 more ...
| ([] | [][])[])[])[])[])[])[])[])[])[])[])[])[])[])[])[];
}>
*/
const okay = x.x().x().x().x().x().x().x().x().x().x().x().x().x().x().x().x();
/*
const givesAny: X<{
arr: any[];
}>
*/
const givesAny = okay.x(); It seems like the limit is way lower after that change. 16 calls is the max. And, like the original examples, it doesn't give the max depth error. Can I file a separate bug report about the max depth limit of object type params vs array type params? |
I have another example that is similar to the original examples but this one correctly shows the max depth error, type AppendToArray<ArrT extends any[], T> = (
(
ArrT[number]|
T
)[]
);
interface Data {
arr : number[]
};
type AppendToFoo<C extends Data, N extends number> = (
WhyDoesThisWorkFoo<{arr:AppendToArray<C["arr"], N>}>
);
/**
* Changed to use `number[]`, instead of an object.
*/
class Foo<ArrT extends number[]> {
arr! : ArrT;
//Using `this`
x<N extends number> (n:N) : AppendToFoo<this, N> {
return null!;
}
}
/*
This uses an object, instead of `number[]`
I don't know why this works.
*/
interface WhyDoesThisWorkFoo<DataT extends Data> extends Foo<DataT["arr"]> {}
declare const foo0 : WhyDoesThisWorkFoo<{arr:0[]}>;
/*
const foo9: WhyDoesThisWorkFoo<{
arr: (0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9)[];
}>
*/
const foo9 = foo0
.x(1).x(2).x(3).x(4).x(5).x(6).x(7).x(8).x(9);
/*
This works now,
const foo10: WhyDoesThisWorkFoo<{
arr: (0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10)[];
}>
*/
const foo10 = foo9.x(10);
/*
This works now,
const foo20: WhyDoesThisWorkFoo<{
arr: (0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10
| 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20)[];
}>
*/
const foo20 = foo10.x(11).x(12).x(13).x(14).x(15).x(16).x(17).x(18).x(19).x(20);
const foo24 = foo20.x(21).x(22).x(23).x(24);
/*
I finally get,
Type instantiation is excessively deep and possibly infinite.
*/
const foo25 = foo24.x(25);
// ~~~~~~~~~~~~ Type instantiation is excessively deep and possibly infinite. And I don't know why this has a higher limit than the original examples, even though it is more complex. |
Here's an even more mind boggling example, type AppendToArray<ArrT extends any[], T> = (
(
ArrT[number]|
T
)[]
);
interface Data {
arr : number[]
};
/*
Uses object instead of `number[]`
*/
interface InterfaceFoo<DataT extends Data> {
arr : DataT["arr"];
}
type AppendToFoo<C extends Data, N extends number> = (
ClassFoo<{arr:AppendToArray<C["arr"], N>}>
);
/*
Uses object instead of `number[]`
*/
class ClassFoo<DataT extends Data> implements InterfaceFoo<DataT> {
arr! : DataT["arr"];
//Using `this`
x<N extends number> (n:N) : AppendToFoo<this, N> {
return null!;
}
}
declare const foo0 : ClassFoo<{arr:0[]}>;
/*
const foo9: ClassFoo<{
arr: (0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9)[];
}>
*/
const foo9 = foo0
.x(1).x(2).x(3).x(4).x(5).x(6).x(7).x(8).x(9);
/*
Expected:
Max depth error OR resolve correctly
Actual:
const foo10: ClassFoo<{
arr: any[];
}>
*/
const foo10 = foo9.x(10);
/*
Expected:
Max depth error OR resolve correctly
Actual:
const foo20: ClassFoo<{
arr: any[];
}>
*/
const foo20 = foo10.x(11).x(12).x(13).x(14).x(15).x(16).x(17).x(18).x(19).x(20);
/*
Expected:
Max depth error OR resolve correctly
Actual:
const foo24: ClassFoo<{
arr: any[];
}>
*/
const foo24 = foo20.x(21).x(22).x(23).x(24);
/*
I finally get,
Type instantiation is excessively deep and possibly infinite.
*/
const foo25 = foo24.x(25);
// ~~~~~~~~~~~~ Type instantiation is excessively deep and possibly infinite. It is similar to the However it is also similar to the original example because What's mind boggling is that you do not get any errors and still get I have no idea why. Expected: Actual:
@fatcerberus From I dub it Schrödinger's type resolution |
Also, I'd like to point out that that the above examples show there's something weird with what TS thinks is "too deep". Like, there's not just a bug with errors not appearing. Like maybe the It's just a gut feeling, after having played around with the type system a lot. |
Absolutely - the depth limit is as low as it is simply because if it were much higher you'd get a stack overflow because instantiation is written as a set of mutually recursive functions. That's why I opened #32611 - all we really want to limit is really complex types (instantiation is effectively type execution, given conditionals, and we do not want to let types execute forever ❤️ ), not deep types, and using a trampoline allows us to remove the stack limit-based constraint. |
In my quest for finding a workaround for the original examples, I have found it. It relies on two types.
This means I do not know why it works. @weswigham However, I have also found another weird bug. So, with the Playground code, it'll resolve And it'll switch between resolving successfully and Then, after leaving the tab idle, it'll just.... crash the tab. VS code and |
I'm still experimenting with the workaround on VS code and I cranked it up to As you can see from the above screenshot, the emitted |
[Edit] In the following tests, I only export the very last I went all the way to I'd post the Playground but the URL is too long. So, here's the first part of the repro, /*
The workaround consists of `AppendToFoo` and `AppendToFooImpl`.
`AppendToFoo` will take an object type param.
`AppendToFooImpl` will take one type param per property of the object type param.
*/
type AppendToArray<ArrT extends any[], T> = (
(
ArrT[number]|
T
)[]
);
interface Data {
arr : number[],
otherThing : string,
};
interface InterfaceFoo<DataT extends Data=Data> {
arr : DataT["arr"];
otherThing : DataT["otherThing"];
}
/**
* You **must not** pass `Data` to this type.
* Instead, you must pass one type param per `Data` property.
*/
type AppendToFooImpl<N extends number, ArrT extends number[], OtherThingT extends string> = (
ClassFoo<{
arr:AppendToArray<ArrT, N>,
otherThing : (
OtherThingT extends "haha" ?
"hehe" :
OtherThingT extends "hehe" ?
"hihi" :
"haha"
),
}>
);
/**
* You may also use `Data` instead of `InterfaceFoo`
*/
type AppendToFoo<InterfaceT extends InterfaceFoo, N extends number> = (
AppendToFooImpl<N, InterfaceT["arr"], InterfaceT["otherThing"]>
);
class ClassFoo<DataT extends Data> implements InterfaceFoo<DataT> {
arr! : DataT["arr"];
otherThing! : DataT["otherThing"];
x<N extends number> (_n:N) : AppendToFoo<this, N> {
return null!;
}
}
declare const foo0 : ClassFoo<{arr:0[], otherThing:"Hello"}>; Then, use this to generate as many cases as necessary, function chain (nStart, nEnd) {
const arr = [];
for (let i=nStart; i<=nEnd; ++i) {
arr.push(`.x(${i})`);
}
return `const foo${nEnd} = foo${nStart-1}` + arr.join("") + ";";
}
function chainMore () {
arr = [];
for (let i=1; i<10000; i+=10) {
arr.push((i+10<10000 ? "" : "export ") + chain(i, i+9));
}
return arr.join("\n");
}
chainMore() [Edit] Modified the code above to generate an With my configured
With emit enabled and
And my configured {
"compilerOptions": {
"sourceMap": true,
"declarationMap": true,
"declaration": true,
"diagnostics": true,
"extendedDiagnostics": true,
"composite": true,
"experimentalDecorators": false,
"allowJs": false,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"alwaysStrict": true,
"newLine": "lf",
"noEmitOnError": true,
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitUseStrict": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noErrorTruncation": true,
"strict": true,
"module": "commonjs",
"moduleResolution": "node",
"target": "esnext",
"lib": ["esnext"],
"jsx": "react",
"jsxFactory": "React.createElement"
},
"compileOnSave": false
}
So, it seems like It seems like At 590, TL;DR
Also, I ran these builds a bunch of times and it seems like the max limit is consistent. If it crashes with 5307, it'll always crash with 5307. If it works with 5306, it'll always work with 5306. I did modify my |
I just tested this workaround on Using With emit enabled [Edit] Did I accidentally stumble upon a trampoline-like workaround? o.0 |
Seems like the max limit goes down as version numbers go up. Makes sense. The compiler gets more complex. I'm surprised it doesn't go down much, though.
|
Just leaving a note behind for anyone who may follow after me (future me?), There is another way to sidestep TS' max depth limit and apparent "type amnesia". For this hack/optimization to work,
The workaround is here, We use a bunch of conditional types to check if we can "re-use" an existing type from the type params. The most important part of the workaround is here, If we have found a re-usable type, we re-use it and do not "create" a new type. If you "create" a new type, TS doesn't realize that the newly "created" type is the same as an existing type, and this will incur a max-depth penalty. If you re-use an existing type, you sidestep the issue. And the tests showing 100+ nested function calls,
Before this hack, I could only get 37 nested calls. The 38th would error. |
Okay, better version of the hack. The type that makes it possible is this, The ... Well, whatever. Future me problem. And an extra test, |
TypeScript Version: 3.5.1
Search Terms:
Code
Using
DataT
,Playground
Using
this
,Playground
Expected behavior:
Both examples should resolve correctly or trigger,
Type instantiation is excessively deep and possibly infinite.ts(2589)
See code for more details.
Actual behavior:
Both examples resolve to
any
without errors.Both examples seem to have a different limit before this happens?
See code for more details.
Playground Link:
Using
DataT
, the limit seems to be12
.Playground
Using
this
, the limit seems to be9
.Playground
Related Issues:
I have no clue how to search for this.
I came across this weird bug by accident, actually.
I was working on a personal project and this would be the 4th, or 6th time I've rewritten the project. I've noticed that in all the rewrites, there's this particular method on generic class where the max number of times I could chain it was always 20.
The 21st call would always trigger the max depth error.
I was sick of it and decided to investigate possible ways to work around this limit.
I decided to write a simple repro before messing with it. (The above code snippets).
However, the simplified repro behaved very weirdly, and would not trigger the error.
It boggles me how TS can resolve crazy types like this,
but will choke on simple types like the above snippets.
It's also super weird because my super complex examples have a limit of 20 calls. And these super simple examples have a super low limit.
The text was updated successfully, but these errors were encountered: