Repository navigation
Undetected illegal assignment of nested array types #52912
Copy link
Copy link
Closed
Labels
BugA bug in TypeScriptA bug in TypeScriptCursed?It's likely this is extremely difficult to fix without making something else much, much worseIt's likely this is extremely difficult to fix without making something else much, much worseDomain: check: Error InstabilityErrors appear or disappear based on order of checker operations, e.g. LS / tsc discrepanciesErrors appear or disappear based on order of checker operations, e.g. LS / tsc discrepanciesFixedA PR has been merged for this issueA PR has been merged for this issueHelp WantedYou can do thisYou can do this
Milestone
Description
Bug Report
Typescript does not allow the assignment of Source4 to Target4 which is expected because they are incompatible. After 3 or more levels of nested array properties this error is not detected anymore. Even the errors on the nested types may disappear depending on the order of the assignments.
Note: every type is explicitly named and there are no cycles.
(This actually caused problems in our production codebase)
🔎 Search Terms
nested array assignment, undetected illegal assignment of nested array types, undetected illegal assignment, illegal assignment
🕗 Version & Regression Information
- In versions before 4.5.3: limit of nested array levels is 5 instead of 3
- This changed between versions 4.4.4 and 4.5.3 (to 3 levels)
- PRs:
- bisect: 🤖 Pick PR #46599 (Improve recursion depth checks) into release-4.5 #46974 (thank you pyBlob for bisect)
- main: Improve recursion depth checks #46599
- maybe:
isDeeplyNestedTypemaxDepth check?
- maybe:
⏯ Playground Link
Playground link with relevant code
💻 Code
type Source1 = { array: Source2[] };
type Source2 = { array: Source3[] };
type Source3 = { array: Source4[] };
type Source4 = {};
// same as Source types but with "someNewProperty"
type Target1 = {
array: Target2[];
// someNewProperty: string // error in target1 assignment as expected if enabled
};
type Target2 = {
array: Target3[];
// someNewProperty: string // error in target1 assignment as expected if enabled
};
type Target3 = {
array: Target4[];
// someNewProperty: string // error in target1 assignment as expected if enabled
};
type Target4 = {
someNewProperty: string; // not existing in Source4 => no error in target1 assignment
};
declare const source1: Source1;
declare const source2: Source2;
declare const source3: Source3;
declare const source4: Source4;
// this should not compile:
const target1: Target1 = source1; // comment this line to get errors in target2, target3 assignments or move this line after target2 assignment
// this should not compile:
const target2: Target2 = source2; // error if target1 assignment is commented or after this assignment
// this should not compile:
const target3: Target3 = source3; // error if target1 assignment is commented or after target2 assignment
// does not compile:
const target4: Target4 = source4; // error as expected
/*
PS:
- same (wrong) behavior with interfaces and classes
*/🙁 Actual behavior
Source1is assignable toTarget1Source2is assignable toTarget2Source3is assignable toTarget3- Reordering of assignments impact type checking
- If
source1assignment is skipped or moved aftersource2assignment the expected errors show
- If
🙂 Expected behavior
Source1is not assignable toTarget1
Type 'Source1' is not assignable to type 'Target1'.
Types of property 'array' are incompatible.
Type 'Source2[]' is not assignable to type 'Target2[]'.
Type 'Source2' is not assignable to type 'Target2'.
Types of property 'array' are incompatible.
Type 'Source3[]' is not assignable to type 'Target3[]'.
Type 'Source3' is not assignable to type 'Target3'.
Types of property 'array' are incompatible.
Type 'Source4[]' is not assignable to type 'Target4[]'.
Property 'someNewProperty' is missing in type 'Source4' but required in type 'Target4'.
- Since
Source1is a fixed/static tree (without any self references) I expect the error to be found by the compiler. - Reordering of assignments do not impact type checking results
Related issues
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
BugA bug in TypeScriptA bug in TypeScriptCursed?It's likely this is extremely difficult to fix without making something else much, much worseIt's likely this is extremely difficult to fix without making something else much, much worseDomain: check: Error InstabilityErrors appear or disappear based on order of checker operations, e.g. LS / tsc discrepanciesErrors appear or disappear based on order of checker operations, e.g. LS / tsc discrepanciesFixedA PR has been merged for this issueA PR has been merged for this issueHelp WantedYou can do thisYou can do this
Activity
RyanCavanaugh commentedon Feb 22, 2023
The root cause here is that we need to bail out at some point during nested evaluation of a type, since we have no way to know that we're not encountering an infinitely generatively-nested recursive type, e.g.:
"Ah, but this type is just an ever-deeper instantiation of the same thing over and over again, which would be trivial to detect, whereas OP is totally different" you say. But the stack here looks just like the stack in the original report, because we keep visiting ever-deeper instantiations of the same type --
Array<T>. The same behavior can be seen with a non-array generic type:So if anyone wants to try to fix this they're welcome to come up with some new detection mechanism, but any fix here needs to a) not tank performance in normal scenarios and b) not introduce new circularity or complexity errors in real code either.
Note that it's also not sufficient to detect
Nestedvia the instantiations being "adjacent" in the call stack -Nestedmight ping-pong with another type (or even a series of other types, via conditional types) as it marches toward infinity.martinwepner commentedon Feb 25, 2023
Hi Ryan Cavanaugh (@RyanCavanaugh),
thank you for your quick response! I think "Cursed?" is the right label for this. What actually surprised me was that this problem has not been reported yet.
When using Typescript-based ORMs like Prisma (@prisma), you can run into this kind of problem very quickly when joining some tables. In our case, the following data structure triggered the error (simplified):
Order->Payment[]->Refund[]->RefundOrderItemAmount[]`.
I am not familiar with the TS codebase, but given your example of the infinitely recursive
Nestedtype, I would indeed have asked why it is so hard to detect. When building a "type tree" (I don't know the correct term) forSource1, there are no cycles or self-references (so there is no path through the tree back to theSource1type) compared to theNestedtype, which quickly references back to itself (Nested.b->Nested==> cycle detected; no tree => treat differently than a tree).Naively, isn't it trivial to discover the circle here too?‼️ cycle detected.
Type to parse
Ping:Ping.pong=>Pong.ping=>PingIf you parse conditional types isn't it possible to parse all possible ways to also be able to detect a tree vs a graph?
But as I said I know nothing about the typescript codebase and probably one can easily find a more complex example that is not that trivial to detect.
But anyway, I find both your Source-Box and my Source-Array example scary. Typescript promises to be "A Result You Can Trust" and this is a very simple and not so far-fetched example where typescript breaks that promise.
If you could give me some starting points within the codebase to investigate this problem, I would be happy to help find a solution.
RyanCavanaugh commentedon Feb 27, 2023
That's not what's happening in the provided example. You're talking about detecting cycles, but we're not talking about cycles here, but rather infinite descent of novel types the whole way down. In other words, although there are referential cycles, there are not cycles of the same types - we're talking about increasingly-deep instantiations of the same types.
You can add this line: main...RyanCavanaugh:TypeScript:stackDemo#diff-d9ab6589e714c71e657f601cf30ff51dfc607fc98419bf72e04f6b0fa92cc4b8R22654 to remove the depth limit. Then start trying to figure out how to make all the newly-failing tests in the same commit pass again, e.g. this one: main...RyanCavanaugh:TypeScript:stackDemo#diff-b92eac93adfa5c33adb53f3e9689088003bc273d7ec04382d0573f6944055897R25
This testcase, shown here failing if the depth limit didn't exist, is a good demonstration. In order to see if
B<T>is a legalA<T>, you have to be able to answer the question of whetherA<B<A<B<A<B<A<B<A<B<T ....is related toB<A<B<A<B<A<B<A<B<A<T ..., at infinite depth. But you can't go infinitely deep, and you can't just say "no" ("no" is the wrong answer here), so you have to have some strategy that terminates in finite time but still produces the most-possibly-correct answers.Fair warning: if we had any idea how to fix this, it wouldn't be Cursed (and would have been fixed already). PRs surely welcomed but this isn't a case of us simply not having tried to fix it before.
Fix isDeeplyNestedType false positive for chains of distinct array ty…
Add repro test for issue microsoft#52912 nested array assignability
Clarify microsoft#52912 regression test intent and update baselines
RyanCavanaugh commentedon Sep 19, 2026
Current native TypeScript 7.1.0-dev.20260918.1 now reports TS2322 for
Source1assigned toTarget1, as well as the correspondingSource2/Target2andSource3/Target3assignments. It still reports the expected missingsomeNewPropertyerror forSource4/Target4.Classic TypeScript 6.0.3 still reports only the
Source4/Target4error, but the reported nested-array assignment is fixed in the current TypeScript 7 implementation.