Repository navigation
Undetected illegal assignment of nested array types #52912
Description
Activity
- addedBugA bug in TypeScriptA bug in TypeScriptHelp WantedYou can do thisYou can do thisCursed?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 worse
on Feb 22, 2023 RyanCavanaugh commented
on Feb 22, 2023 MemberMore actionsThe 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.:
type Nested<T> = { a: T; b?: Nested<Nested<T>>; }
"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:type Box<T> = { value: T }; type Source1 = { array: Box<Source2> }; type Source2 = { array: Box<Source3> }; type Source3 = { array: Box<Source4> }; type Source4 = {}; // same as Source types but with "someNewProperty" type Target1 = { array: Box<Target2>; // someNewProperty: string // error in target1 assignment as expected if enabled }; type Target2 = { array: Box<Target3>; // someNewProperty: string // error in target1 assignment as expected if enabled }; type Target3 = { array: Box<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
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.Reacted by Omri LuzonHi 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).type Ping = { pong: Pong }; type Pong = { ping: Ping };
Naively, isn't it trivial to discover the circle here too?
Type to parsePing:Ping.pong=>Pong.ping=>Ping‼️ cycle detected.
If 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 commented
on Feb 27, 2023 MemberMore actionsNaively, isn't it trivial to discover the circle here too?
Type to parse Ping: Ping.pong=>Pong.ping=>Ping‼️ cycle detected.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.
If you could give me some starting points within the codebase to investigate this problem, I would be happy to help find a solution.
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.
- addedDomain: 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 discrepancies
on Oct 16, 2025 RyanCavanaugh commented
on Sep 19, 2026 MemberMore actionsCurrent 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.Reacted by martinwepner- addedFixedA PR has been merged for this issueA PR has been merged for this issueNeeds Human ReviewThis issue has a backlog check awaiting maintainer review.This issue has a backlog check awaiting maintainer review.
on Sep 19, 2026 - removedNeeds Human ReviewThis issue has a backlog check awaiting maintainer review.This issue has a backlog check awaiting maintainer review.
on Sep 23, 2026
Bug Report
Typescript does not allow the assignment of
Source4toTarget4which 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
isDeeplyNestedTypemaxDepth check?⏯ Playground Link
Playground link with relevant code
💻 Code
🙁 Actual behavior
Source1is assignable toTarget1Source2is assignable toTarget2Source3is assignable toTarget3source1assignment is skipped or moved aftersource2assignment the expected errors show🙂 Expected behavior
Source1is not assignable toTarget1Source1is a fixed/static tree (without any self references) I expect the error to be found by the compiler.Related issues
#42070