Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
someBrown opened this issue Mar 1, 2022 · 11 comments Β· May be fixed by #48092
Assignees
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue Rescheduled This issue was previously scheduled to an earlier milestone

Comments

@someBrown
Copy link

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

@jcalz
Copy link
Contributor

jcalz commented Mar 1, 2022

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

@jcalz
Copy link
Contributor

jcalz commented 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
Copy link
Member

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

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Mar 1, 2022
@RyanCavanaugh RyanCavanaugh changed the title extends bug Conditional type evaluation of type aliases produces different result than their equivalent substitution Mar 1, 2022
@ahejlsberg
Copy link
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.

@ahejlsberg ahejlsberg added Bug A bug in TypeScript and removed Needs Investigation This issue needs a team member to investigate its status. labels Mar 2, 2022
@ahejlsberg ahejlsberg added this to the TypeScript 4.7.0 milestone Mar 2, 2022
@typescript-bot typescript-bot added the Fix Available A PR has been opened for this issue label Mar 2, 2022
@RyanCavanaugh RyanCavanaugh added the Rescheduled This issue was previously scheduled to an earlier milestone label May 13, 2022
@ddurschlag
Copy link

Not sure if this is a separate bug (happy to file it -- maybe @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.

@whzx5byb
Copy link

whzx5byb commented Oct 7, 2023

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

@ddurschlag
Copy link

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

@ahejlsberg
Copy link
Member

ahejlsberg commented Oct 7, 2023

@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.

@ehmicky
Copy link

ehmicky commented 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.

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} & {};

@bergwerf
Copy link

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..

@jcalz
Copy link
Contributor

jcalz commented Mar 22, 2025

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
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue Rescheduled This issue was previously scheduled to an earlier milestone
Projects
None yet
Development

Successfully merging a pull request may close this issue.

9 participants