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
Generic type alias produces erroneously compatible types #31006
Search Terms:
generics, resolved, nested generics, specified, assignable, type safety, type alias, compatible
Code
interfaceA{a: number;}interfaceB{b: string;}typeDemo<T>=<KextendskeyofT>(key: K,val: T[K])=>void// Demo<A> should not be assignable to Demo<B>, but it is?!declareconstimplDemoA: Demo<A>;constimplDemoB: Demo<B>=implDemoA;// no error!? 😕
Expected behavior:
There should be an error that a type Demo<A> is not assignable to Demo<B>.
Actual behavior:
It compiles with no error.
I guess the compiler "forgets" what concrete type T is, since it doesn't appear directly in the resulting type. If one manually substitutes A and B for T into the definition of Demo<T> to make DemoA and DemoB, everything behaves as expected:
// Note that we can manually make a concrete type DemoA and DemoB:typeDemoA=<KextendskeyofA>(key: K,val: A[K])=>void;typeDemoB=<KextendskeyofB>(key: K,val: B[K])=>void;typeMutuallyAssignable<TextendsU,UextendsV,V=T>=true;// the compiler agrees that DemoA and Demo<A> are mutually assignabledeclareconsttestAWitness: MutuallyAssignable<Demo<A>,DemoA>;// okay// the compiler agrees that DemoB and Demo<B> are mutually assignabledeclareconsttestBWitness: MutuallyAssignable<Demo<B>,DemoB>;// okay// And the compiler *correctly* sees that DemoA is not assignable to DemoBdeclareconstimplDemoAConcrete: DemoA;constimplDemoBConcrete: DemoB=implDemoAConcrete;// error as expected// ~~~~~~~~~~~~~~~~~ <-- Type 'DemoA' is not assignable to type 'DemoB'.
The key issue here is that we erase function type parameters (i.e. replace them with any) when we relate signatures that are each instantiations of the same underlying signature. Extract from signaturesRelatedTo:
if(getObjectFlags(source)&ObjectFlags.Instantiated&&getObjectFlags(target)&ObjectFlags.Instantiated&&source.symbol===target.symbol){// We have instantiations of the same anonymous type (which typically will be the type of a// method). Simply do a pairwise comparison of the signatures in the two signature lists instead// of the much more expensive N * M comparison matrix we explore below. We erase type parameters// as they are known to always be the same.for(leti=0;i<targetSignatures.length;i++){constrelated=signatureRelatedTo(sourceSignatures[i],targetSignatures[i],/*erase*/true,reportErrors);if(!related){returnTernary.False;}result&=related;}}
This obviously looses some information, but that's generally fine as long as the type parameters have no constraints, non-generic constraints, or constraints that reference other type parameters in the same type parameter list, because they're then known to be identical in the two signatures we're relating. However, when type parameters have constraints that reference outer type parameters (as is the case in the Demo<T> type above) we have an issue. In this particular example we end up getting the variance probing wrong and then fall back to a structural comparison that gets it even more wrong (because we resolve T[any] for a non-generic T to just any).
I think the solution is to apply getBaseSignature to each of the signatures before we relate them. That instantiates type parameters to their constraints instead of any. Doing so reveals another issue (we're not relating callbacks with type predicates correctly and Array<T> uses that in its filter method) but I think that's fixable too.
Wesley Wigham (@weswigham) No, the current ordering is good. We definitely want to first recognize instantiations of the same signature because that tells us that function type parameters (if any) are identical, allowing us to erase them to their constraints.
Activity
RyanCavanaugh commentedon Apr 18, 2019
Behavior dates back to TS 2.1 when the syntax was first legal.
RyanCavanaugh commentedon Apr 18, 2019
And it's not like we don't know
🤷♀️
ahejlsberg commentedon Apr 18, 2019
The key issue here is that we erase function type parameters (i.e. replace them with
any) when we relate signatures that are each instantiations of the same underlying signature. Extract fromsignaturesRelatedTo:This obviously looses some information, but that's generally fine as long as the type parameters have no constraints, non-generic constraints, or constraints that reference other type parameters in the same type parameter list, because they're then known to be identical in the two signatures we're relating. However, when type parameters have constraints that reference outer type parameters (as is the case in the
Demo<T>type above) we have an issue. In this particular example we end up getting the variance probing wrong and then fall back to a structural comparison that gets it even more wrong (because we resolveT[any]for a non-genericTto justany).I think the solution is to apply
getBaseSignatureto each of the signatures before we relate them. That instantiates type parameters to their constraints instead ofany. Doing so reveals another issue (we're not relating callbacks with type predicates correctly andArray<T>uses that in itsfiltermethod) but I think that's fixable too.weswigham commentedon Apr 18, 2019
I think we should proooobably also move the
sourceSignatures.length === 1 && targetSignatures.length === 1case to the top.ahejlsberg commentedon Apr 19, 2019
Wesley Wigham (@weswigham) No, the current ordering is good. We definitely want to first recognize instantiations of the same signature because that tells us that function type parameters (if any) are identical, allowing us to erase them to their constraints.
24 remaining items
DetachHead commentedon Sep 20, 2022
it looks like this was fixed years ago lol
playground
RyanCavanaugh commentedon Sep 20, 2022
Indeed!