Skip to content

Conditional type evaluation of type aliases produces different result than their equivalent substitutionΒ #48070

Description

@someBrown

Bug Report

πŸ”Ž Search Terms

extends

πŸ•— Version & Regression Information

v4.5.4

⏯ Playground Link

Playground link with relevant code

πŸ’» Code

type S<X> = <T>() => T extends X ? 1 : '2'
type Foo = S<'s1'> 
type Foo2 = S<'s2'>
type Result1 = Foo extends Foo2 ? true : false 

type Result2 = S<'s1'> extends S<'s2'> ? true : false

πŸ™ 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

Activity

  1. jcalz commented on Mar 1, 2022

    @jcalz
    Contributor

    Version/regression info: behavior changed between 4.2.0-dev.20210111 and 4.2.0-dev.20210112

  2. jcalz commented on Mar 1, 2022

    @jcalz
    Contributor

    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. πŸ‘¦β€πŸ•΅οΈβ€β™‚οΈ

  3. RyanCavanaugh commented on Mar 1, 2022

    @RyanCavanaugh
    Member

    Anders Hejlsberg (@ahejlsberg) mentioned yesterday he was looking into variance computation, and this seems relevant...

  4. changed the title [-]extends bug[/-] [+]Conditional type evaluation of type aliases produces different result than their equivalent substitution[/+] on Mar 1, 2022
  5. ahejlsberg commented on Mar 2, 2022

    @ahejlsberg
    Member

    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.

  6. added
    BugA bug in TypeScript
    and removed
    Needs InvestigationThis issue needs a team member to investigate its status.
    on Mar 2, 2022
  7. ddurschlag commented on Oct 6, 2023

    @ddurschlag

    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.

  8. whzx5byb commented on Oct 7, 2023

    @whzx5byb

    ddurschlag I think this is because string & number is eagerly reduced to never when used as a generic type parameter.

  9. ddurschlag commented on Oct 7, 2023

    @ddurschlag

    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 :(

  10. ahejlsberg commented on Oct 7, 2023

    @ahejlsberg
    Member

    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.

  11. ehmicky commented on Jan 15, 2024

    @ehmicky

    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} & {};
  12. bergwerf commented on Mar 22, 2025

    @bergwerf

    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 number is mysterious to me. It implies that number | number[] extends number is ambiguous..

  13. jcalz commented on Mar 22, 2025

    @jcalz
    Contributor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

BugA bug in TypeScriptDomain: Conditional TypesThe issue relates to conditional typesRescheduledThis issue was previously scheduled to an earlier milestone

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions