Skip to content
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

Generic type changes depending if its parameter type is wrapped or not. #56675

Open
miguel-leon opened this issue Dec 5, 2023 · 3 comments
Open
Assignees
Labels
Needs Investigation This issue needs a team member to investigate its status.

Comments

@miguel-leon
Copy link

πŸ”Ž Search Terms

generic type bug

πŸ•— Version & Regression Information

latest version as of today.

⏯ Playground Link

https://www.typescriptlang.org/play?#code/C4TwDgpgBAkgZjAJhAdsAlgYwIYBsA8AGgDRQCapAglALxTABOArhKQEK1Rx4DOEAfLQBQUUVAAU+ACr9xASlqCpUCAA9gqRDyiEoAfigBGKAC4oAJgVqNKLSLGSZ8xVGXXN2svqOmLCg9RmbADcQmGgkLBwAPIM+AAKpACKpFLM0HSMLKQAYrwZXPmCdPBIqBg4BImFuHykpchoWHj4KTV1UHm1rK7p-Kl9oUIR0AD6xiUxcVk9M-zBUAD0i-Tpw+Bj5pzwsfgzpNzd80srM+uRowDM21P4hx1zC8urLOdjACw3u-c9P8fPPzCb1gPAAyox0CgAObSYpRMpNSrSUg8CHQ+bhDZQUYAVm2YLRMJQTAAtgAjCAMDFAkbYqQQVETKLffL1AkMSFE0kUqn-FY-AB0UEwAHsGAwIJhgABCIHAnYMADq6GAAAt4OCOdDYV84j82ZrObDqbTRvTUVtJrFlWqNYT8MTyZS+S8IAYyUwobKgA

πŸ’» Code

type IfIdentical<X, Y, A = true, B = false> =
    (<T>() => T extends X ? 1 : 2) extends
    (<T>() => T extends Y ? 1 : 2) ? A : B;


type IfOr<P, Q, True = true, False = false> = IfIdentical<P, false, IfIdentical<Q, false, False, True>, True>;

type _1 = IfOr<true, true>; // true
type _2 = IfOr<true, false>; // true
type _3 = IfOr<false, true>; // true
type _4 = IfOr<false, false>; // false


type IsString<T> = IfIdentical<T, string>;

type _5 = IsString<number>;



type _Test1 = IfOr<false, IsString<number>>; // false. correct!



type IfOrWithIfString<T> = IfOr<false, IsString<T>>;

type _Test2 = IfOrWithIfString<number>; // true? bug!

πŸ™ Actual behavior

types _Test1 and _Test2 are different even though they are calculated the same way.

πŸ™‚ Expected behavior

types _Test1 and _Test2 should be the same.

Additional information about the issue

Not sure if it's the best minimal reproduction, because it's difficult to pinpoint the actual problem.
The example is based on the a type suggested by Matt McCutchen here, but I can't tell if the issue is unique to that type or if it happens in other scenarios.

Based on that IsEqual type I created a "if or" type IfOr<P, Q> and a "is string" type IsString<T>.

The difference between _Test1 and _Test2 is that the first passes IsString<number> result (false) directly to IfOr, while the second passes number to an intermediary type, sort of wrapping the IsString.

@RyanCavanaugh
Copy link
Member

IsIdentical and/or IsOr is written incorrectly, resulting in a difference between argument-base and structure-based inference, same as might occur if you wrote interface DoesNotUseT<T> { x: string } and compared inference with DoesNotUseT<number> - DoesNotUseT<U> to { x: string } - DoesNotUseT<U>. In both cases, the exact order of when things are instantiated can cause the result to change. The fix is to write IsIdentical correctly.

The divergence in expectation actually starts one line earlier:

type IfOrWithIsString<T> = IfOr<false, IsString<T>>;
//   ^?
//     type IfOrWithIsString = true, wat?

// Which branch of IfOr is being hit? Let's label them
type IfOrSimpler1<P, Q> = IfIdentical<P, false, IfIdentical<Q, false, "Both False", "Q is not false">, "P is not false">;
type IfOrSimpler1WithIfString<T> = IfOrSimpler1<false, IsString<T>>;
//   ^?
//   "Q is not false"
// It's hitting IsIdentical<IsString<T>, false>. Why?

// Simplify IfOr to just the P case
type IfOrSimpler2<Q> =  IfIdentical<Q, false, "Both False", "Q is not false">;
type IfOrSimpler2WithIsString<T> = IfOrSimpler2<IsString<T>>;
//   ^?
//   ^? "Q is not false"
// IfIdentical<IsString<T>, false> hits the 'false' condition

type IfIdenticalSimpler<X, Y> =
    (<T>() => T extends X ? 1 : 2) extends
    (<T>() => T extends Y ? 1 : 2) ? "Seems to extend" : "Seems not to";
type IfSimple3<Q> =  IfIdenticalSimpler<Q, false>;
type IfSimple3WithIsString<T> = IfSimple3<IsString<T>>;
//   ^?
//   ^? "Seems not to"

You could instead write this, which works more reliably:

type IfOr<P, Q, True = true, False = false> = IfIdentical<P, true, True, IfIdentical<Q, true, True, False>>;

or use a more usual definition of IsIdentical:

type IfIdentical<X, Y, A = true, B = false> = X extends Y ? Y extends X ? A : B : B;

TL;DR you can't take falsy results of conditional type evaluations as definitive. Restrictive instantiations can and do occur; for most conditional types, they should be of the form A extends B ? X : X | Y to produce correct results.

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Dec 5, 2023
@RyanCavanaugh
Copy link
Member

cc @weswigham please check my work on the above πŸ˜…

@sliminality
Copy link

sliminality commented Jul 3, 2024

I'm not sure I follow this:

you can't take falsy results of conditional type evaluations as definitive

Are you saying that if my conditional type A extends B ? X : Y evaluates Y, I cannot rule out the case that A extends B?

Restrictive instantiations can and do occur

I have seen this terminology ("restrictive instantiation") in the codebase, but I don't fully understand it.

Given the following code

type Foo<T> = …
type Bar<U extends string> = …
type Qux<V extends "v1" | "v2"> = …

would the most restrictive and permissive instantiations be as follows:

Parameter Most restrictive Most permissive
T unknown any
U string any
V "v1" | "v2" any

More generally:

  1. Given a type parameter T extends Foo, is the "most restrictive" instantiation always Foo, or unknown if T is not bound?
  2. Is the permissive instantiation of a type always any?

Also, when does a restrictive instantiation occur, and how are restrictive/permissive instantiations used to determine whether to evaluate conditional types? I tried to read the code but got confused. It seems to me that if you have

type Foo<A extends A'> = A extends B ? X : Y

where $A &lt;: A' &lt;: B$, then any well-typed Foo<A> can be simplified to X without evaluating A extends B. Conversely, for

type Bar<A extends A'> = A extends B ? X : Y

where $A &lt;: A' \not&lt;: B$ (meaning $A'$ and $B$ are wholly incomparable, like string and number), then any well-typed Foo<A> can be simplified to Y without evaluating A extends B. But I'm not sure how these observations tie into restrictive/permissive instantiations.

for most conditional types, they should be of the form A extends B ? X : X | Y to produce correct results.

Is this saying that most conditional types should be written in the form A extends B ? X : (X | Y) (parens added for clarity), meaning instead of something like

type Config<T extends "dev" | "stg" | "prod"> = T extends "prod" ? "safe config" : "allow experiments"

we should actually write

type Config<T extends "dev" | "stg" | "prod"> = T extends "prod" ? "safe config" : ("allow experiments" | "safe config")

otherwise Config<"prod"> could still be judgmentally equal to "allow experiments" under certain conditions (which)?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Needs Investigation This issue needs a team member to investigate its status.
Projects
None yet
Development

No branches or pull requests

4 participants