You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Check order dependence with mutually-recursive non-unary generics #44572
interfaceParent<A,B>{getChild(): Child<A,B>;iter(): Iterable<Parent<A,B>>;}interfaceChild<A,B>extendsParent<A,B>{readonlya: A;readonlyb: B;}classImpl<A,B>implementsParent<A,B>{constructor(readonlychild: Child<A,B>){}getChild(): Child<A,B>{returnthis.child;}*iter(): Iterable<Parent<A,B>>{constmap=newMap<Child<unknown,unknown>,Child<A,B>[]>();function*gen(inp: Child<A,B>): Iterable<Child<A,B>>{yield*map.get(inp)||[];}}}constx: Parent<unknown,unknown>={}asany;const_: Parent<null,unknown>=x;// should not pass
The final assignment should not pass. It accurately errors in 3.7.5, if the Impl class is removed (or any aspect of the iter implementation is modified), or by adding a sentinel type like sentinel?: A to the Parent interface to aid in type checking.
🙁 Actual behavior
No error is thrown in the final assignment.
🙂 Expected behavior
A n error is throw:
Type 'Parent<unknown, unknown>' is not assignable to type 'Parent<null, unknown>'.
Type 'unknown' is not assignable to type 'null'.
changed the title [-]Typescript passing incorrect assignability check in very strange circumstance.[/-][+]Adding an implementation of an interface allows an invalid assignability check of the interface to pass[/+]on Jun 13, 2021
changed the title [-]Adding an implementation of an interface allows an invalid assignability check of the interface to pass[/-][+]Check order dependence with mutually-recursive non-unary generics[/+]on Jun 14, 2021
Simplified and cleaned up a little to make the violation more apparent
interfaceParent<A>{child: Child<A>|null;parent: Parent<A>|null;}interfaceChild<A,B=unknown>extendsParent<A>{readonlya: A;// This field isn't necessary to the repro, but the// type parameter is, so including itreadonlyb: B;}functionfn<A>(inp: Child<A>){// This assignability check defeats the later oneconsta: Child<unknown>=inp;}// Allowed initialization of puconstpu: Parent<unknown>={child: {a: 0,b: 0,child: null,parent: null},parent: null};// Should errorconstnotString: Parent<string>=pu;// Unsound read on .child.aconstm: string=notString.child!.a;
Debugging notes: variance measurement for Parent is getting set to VarianceFlags.Independent, implying that its type parameter is never witnessed at all. It arrived at this conclusion by checking the assignability of Parent instantiated with marker types. It first checks assignability in both directions with instantiations with super/sub-related marker types, and
assignability appears to return true in both directions; however, it actually is returning Ternary.Unknown, due to being unable to answer questions about the assignability of the types' parent and child properties without knowing their variances. After (incorrectly) concluding that Parent is bivariant on A, it checks another set of instantiations with markers that are unrelated to each other. That too comes back as Ternary.Unknown but is interpreted as true, so the variance gets updated to Independent, since instantiating Parent with all kinds of different markers with different assignability to each other apparently had no effect on the instantiations' assignability to each other.
I'm not sure if any of those comparisons ever actually looked at a and b, which should provide some non-recursive concrete variance information. I'm also not sure if outofbandVarianceMarkerHandler should have been called at some point, but it was not.
I've spent the last 1.5 days debugging something that looks awfully like this one. This is way beyond my head and the understanding of the type system so I can't fully assess if my issue is exactly the same or just similar. Therefore I'm going to post it, for the time being, to avoid creating a new issue that might be a duplicate of this one.
declareclassStateNode<TContext,TEventextends{type: string}>{// if I comment out this property then error is never reported_storedEvent: TEvent// if I comment out this property then error is always reported_action: ActionObject<TEvent>// if I comment out this property then error is always reported_state: StateNode<TContext,any>;}interfaceActionObject<TEventextends{type: string}>{exec: (meta: StateNode<any,TEvent>)=>void}exportdeclarefunctioncreateMachine<TEventextends{type: string}>(action: ActionObject<TEvent>): StateNode<any,any>exportdeclareconstexecute: <TEventextends{type: string}>(handler: (event: TEvent)=>any)=>ActionObject<TEvent>;exportdeclarefunctioninterpret<TContext>(machine: StateNode<TContext,any>): void// uncomment to get a correct report in the `createMachine` call below// const test: ActionObject<{ type: "PLAY"; value: number } | { type: "RESET" }> =// {} as ActionObject<{// type: "PLAY";// value: number;// }>;constmachine=createMachine({}asany);// comment out to get a correct report in the `createMachine` call belowinterpret(machine);createMachine<{type: "PLAY";value: number}|{type: "RESET"}>(execute((ev: {type: "PLAY";value: number})=>{console.log(ev);}));
I know that at this point I'm probably just reiterating what you probably already know but since I've already done this detective work I'm posting this here as a reference, just in case.
I also have a hunch that another issue that I've posted some months ago might be related to this one: #45859 (I would have to confirm this when this one here gets fixed)
Marking this as a design limitation. See rationale here. Note in particular that with #48240 it becomes possible to add variance annotations to establish the correct variance in the examples listed in this issue.
Activity
MartinJohns commentedon Jun 13, 2021
It's reproducible on the playground: Playground link
Deleting the unused
Implclass results in the error being shown.[-]Typescript passing incorrect assignability check in very strange circumstance.[/-][+]Adding an implementation of an interface allows an invalid assignability check of the interface to pass[/+][-]Adding an implementation of an interface allows an invalid assignability check of the interface to pass[/-][+]Check order dependence with mutually-recursive non-unary generics[/+]RyanCavanaugh commentedon Jun 14, 2021
Simplified and cleaned up a little to make the violation more apparent
andrewbranch commentedon Jul 19, 2021
Debugging notes: variance measurement for
Parentis getting set toVarianceFlags.Independent, implying that its type parameter is never witnessed at all. It arrived at this conclusion by checking the assignability ofParentinstantiated with marker types. It first checks assignability in both directions with instantiations with super/sub-related marker types, andassignability appears to return true in both directions; however, it actually is returning
Ternary.Unknown, due to being unable to answer questions about the assignability of the types'parentandchildproperties without knowing their variances. After (incorrectly) concluding thatParentis bivariant onA, it checks another set of instantiations with markers that are unrelated to each other. That too comes back asTernary.Unknownbut is interpreted as true, so the variance gets updated toIndependent, since instantiatingParentwith all kinds of different markers with different assignability to each other apparently had no effect on the instantiations' assignability to each other.I'm not sure if any of those comparisons ever actually looked at
aandb, which should provide some non-recursive concrete variance information. I'm also not sure ifoutofbandVarianceMarkerHandlershould have been called at some point, but it was not.andrewbranch commentedon Aug 20, 2021
Wesley Wigham (@weswigham), I may need some help/advice on this one.
weswigham commentedon Aug 20, 2021
We should probably make a
Ternary.Unknownresult witnessed bygetVarianceWorkerresult in aUnmeasurablevariance.4 remaining items
Andarist commentedon Feb 17, 2022
I've spent the last 1.5 days debugging something that looks awfully like this one. This is way beyond my head and the understanding of the type system so I can't fully assess if my issue is exactly the same or just similar. Therefore I'm going to post it, for the time being, to avoid creating a new issue that might be a duplicate of this one.
TS playground
code from the playground
I've also found out that this has changed between 3.7 and 3.8. More accurately - between
3.8.0-dev.20200117and3.8.0-dev.20200118. If only I've tracked down correctly from which commits those two were built then the behavior change is related to this diff:https://github.com/microsoft/TypeScript/compare/e2e1f6fd85b5076186ba6ee68fe59e11ccac1d3f..afa11d3c7ac37c49fc97230a897e4208ee132ae4
Given this diff, the change was introduced in this PR: #36261
I know that at this point I'm probably just reiterating what you probably already know but since I've already done this detective work I'm posting this here as a reference, just in case.
I also have a hunch that another issue that I've posted some months ago might be related to this one: #45859 (I would have to confirm this when this one here gets fixed)
ahejlsberg commentedon Mar 13, 2022
Marking this as a design limitation. See rationale here. Note in particular that with #48240 it becomes possible to add variance annotations to establish the correct variance in the examples listed in this issue.
fix: add variance annotations to fix file-order-dependent type errors
fix: add variance annotations to fix file-order-dependent type errors
fix: add variance annotations to fix file-order-dependent type errors