Repository navigation
Conditional type evaluation of type aliases produces different result than their equivalent substitutionΒ #48070
Description
Activity
Version/regression info: behavior changed between 4.2.0-dev.20210111 and 4.2.0-dev.20210112
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 commented
on Mar 1, 2022 MemberMore actionsAnders Hejlsberg (@ahejlsberg) mentioned yesterday he was looking into variance computation, and this seems relevant...
- addedNeeds InvestigationThis issue needs a team member to investigate its status.This issue needs a team member to investigate its status.
on Mar 1, 2022 - 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
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.- addedBugA bug in TypeScriptA bug in TypeScriptand removedNeeds InvestigationThis issue needs a team member to investigate its status.This issue needs a team member to investigate its status.
on Mar 2, 2022 - addedFix AvailableA PR has been opened for this issueA PR has been opened for this issue
on Mar 2, 2022 - addedRescheduledThis issue was previously scheduled to an earlier milestoneThis issue was previously scheduled to an earlier milestone
on May 13, 2022 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
neveras the result.ddurschlag I think this is because
string & numberis eagerly reduced toneverwhen used as a generic type parameter.ddurschlag I think this is because
string & numberis eagerly reduced toneverwhen 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 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.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.type A<T> = {a: T extends true[] ? true : false}; type B = A<true[]>; // {a: true} type C = A<boolean[]>; // {a: false} type D = A<true[]> extends C ? true : false; // `false`, which is correct type E = A<true[]> extends A<boolean[]> ? true : false; // `true`, which is incorrect
In that code, the following works around the problem:
type A<in T> = {a: T extends true[] ? true : false};
Another workaround:
type A<T> = {a: T extends true[] ? true : false} & {};
I encountered another instance of this issue:
type Number_Or_Nil<T> = T extends number ? number : T extends [] ? [] : never type T = number | number[] // Evaluates to `never` as expected. let b: T extends number ? number : T extends [] ? [] : never = 0 // Evaluates to `number`..? let a: Number_Or_Nil<T> = 0
The logic behind evaluating the type expression to
numberis mysterious to me. It implies thatnumber | number[] extends numberis ambiguous..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.Reacted by Herman Bergwerf, Ryan Cavanaugh and HuangKaiFeng- addedDomain: Conditional TypesThe issue relates to conditional typesThe issue relates to conditional types
on Oct 16, 2025 - removedFix AvailableA PR has been opened for this issueA PR has been opened for this issue
on Aug 20, 2026
Bug Report
π Search Terms
extends
π Version & Regression Information
v4.5.4
β― Playground Link
Playground link with relevant code
π» Code
π Actual behavior
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
π Expected behavior
type Result1 and type Result2 should return false