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
Conditional type evaluation of type aliases produces different result than their equivalent substitutionΒ #48070
For the result2 expression, the only difference from result1 is that result1 uses the type rather than the specific S<'s1'> ,But they returned a completely different result. Result1 return true,Result2 return false
Looks like #42284 may be the cause, and therefore this is possibly a duplicate of #42421, #44119 (and so maybe #29698 in turn?), etc. Those are listed as "working as intended" (although it seems more like "design limitation" to me, but π€·ββοΈ).
Okay, that's enough amateur sleuthing for me. Hopefully it helps the real TS squad with their triage. π¦βπ΅οΈββοΈ
changed the title [-]extends bug[/-][+]Conditional type evaluation of type aliases produces different result than their equivalent substitution[/+]on Mar 1, 2022
Yeah, there's an issue here. When relating two signatures that were instantiated from the same origin we erase type parameters as they're known to be the same. Since erasure substitutes any for the type parameters, it causes us to resolve the conditional types in the example where really they should stay deferred. I think the fix is to unify the signatures (by instantiating one with the type parameters of the other) instead of erasing their type parameters.
Not sure if this is a separate bug (happy to file it -- maybe Anders Hejlsberg (@ahejlsberg) can comment?) or just an even worse variation of this one.
type IsItNumber<X> = X extends number ? true : false;
type UnsurprisinglyTrue = (string & number) extends number ? true : false;
type SurprisinglyNever = IsItNumber<string & number>;
Not only does going through the IsItNumber alias change the output, it somehow ends up with never as the result.
ddurschlag I think this is because string & number is eagerly reduced to never when used as a generic type parameter.
This seems right. Avoiding primitive types produces more sensible results:
type A = {a: 'a'};
type B = {b: 'b'};
type IsItA<X> = X extends A ? true : false;
type Direct = (A & B) extends A ? true : false;
type Indirect = IsItA<A&B>;
I find this combination of sometimes-eager/sometimes-lazy type evaluation difficult to work with. Why is string&number not eagerly reduced in general? Why are some conditional types eagerly evaluated, and some lazily so? At this point, I want a VSCode plugin that shows what TS thinks of my code internally (e.g. which type expressions are eager, which are lazy, which prevent tail recursive evaluation of conditional types, which are having their type parameters erased...). Even worse, I might want the ability to turn off certain eager evaluation optimizations per-line :(
ddurschlag The difference is that the conditional type in IsItNumber<X> is a distributive conditional type, whereas the conditional type in UnsurprisinglyTrue is not. Distributive conditional types are distributed over union types when instantiated, and since never is the empty union type, a distributive conditional type applied to never always produces never.
No, that's just a distributive conditional type, which depends on whether the T you're checking is generic or not. You gave the name T to both a generic type parameter and a specific type, but they are different things and behave differently in conditional types. This is intended behavior and not an instance of a bug.
Reacted by Herman Bergwerf, Ryan Cavanaugh and HuangKaiFeng
Activity
jcalz commentedon Mar 1, 2022
Version/regression info: behavior changed between 4.2.0-dev.20210111 and 4.2.0-dev.20210112
jcalz commentedon Mar 1, 2022
Looks like #42284 may be the cause, and therefore this is possibly a duplicate of #42421, #44119 (and so maybe #29698 in turn?), etc. Those are listed as "working as intended" (although it seems more like "design limitation" to me, but π€·ββοΈ).
Okay, that's enough amateur sleuthing for me. Hopefully it helps the real TS squad with their triage. π¦βπ΅οΈββοΈ
RyanCavanaugh commentedon Mar 1, 2022
Anders Hejlsberg (@ahejlsberg) mentioned yesterday he was looking into variance computation, and this seems relevant...
[-]extends bug[/-][+]Conditional type evaluation of type aliases produces different result than their equivalent substitution[/+]ahejlsberg commentedon Mar 2, 2022
Yeah, there's an issue here. When relating two signatures that were instantiated from the same origin we erase type parameters as they're known to be the same. Since erasure substitutes
anyfor the type parameters, it causes us to resolve the conditional types in the example where really they should stay deferred. I think the fix is to unify the signatures (by instantiating one with the type parameters of the other) instead of erasing their type parameters.ddurschlag commentedon Oct 6, 2023
Not sure if this is a separate bug (happy to file it -- maybe Anders Hejlsberg (@ahejlsberg) can comment?) or just an even worse variation of this one.
Not only does going through the IsItNumber alias change the output, it somehow ends up with
neveras the result.whzx5byb commentedon Oct 7, 2023
ddurschlag I think this is because
string & numberis eagerly reduced toneverwhen used as a generic type parameter.ddurschlag commentedon Oct 7, 2023
This seems right. Avoiding primitive types produces more sensible results:
I find this combination of sometimes-eager/sometimes-lazy type evaluation difficult to work with. Why is string&number not eagerly reduced in general? Why are some conditional types eagerly evaluated, and some lazily so? At this point, I want a VSCode plugin that shows what TS thinks of my code internally (e.g. which type expressions are eager, which are lazy, which prevent tail recursive evaluation of conditional types, which are having their type parameters erased...). Even worse, I might want the ability to turn off certain eager evaluation optimizations per-line :(
ahejlsberg commentedon Oct 7, 2023
ddurschlag The difference is that the conditional type in
IsItNumber<X>is a distributive conditional type, whereas the conditional type inUnsurprisinglyTrueis not. Distributive conditional types are distributed over union types when instantiated, and sinceneveris the empty union type, a distributive conditional type applied toneveralways producesnever.ehmicky commentedon Jan 15, 2024
I have opened a duplicate issue at #57062 that I am closing in favor of this issue.
However, just for information, this is the code sample. It produces the same problem as above, but slightly differently.
Playground link.
In that code, the following works around the problem:
Another workaround:
bergwerf commentedon Mar 22, 2025
I encountered another instance of this issue:
The logic behind evaluating the type expression to
numberis mysterious to me. It implies thatnumber | number[] extends numberis ambiguous..jcalz commentedon Mar 22, 2025
No, that's just a distributive conditional type, which depends on whether the
Tyou're checking is generic or not. You gave the nameTto both a generic type parameter and a specific type, but they are different things and behave differently in conditional types. This is intended behavior and not an instance of a bug.