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

Conditional Type Inference Bug in TS 5.x #58524

Closed
fxdave opened this issue May 13, 2024 · 13 comments
Closed

Conditional Type Inference Bug in TS 5.x #58524

fxdave opened this issue May 13, 2024 · 13 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@fxdave
Copy link

fxdave commented May 13, 2024

🔎 Search Terms

"Inference trims types", "inference excludes fields"

🕗 Version & Regression Information

  • This is not a crash
  • This changed between versions _4.9 and ___5.0
  • This changed in commit or PR _______
  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about _________
  • I was unable to test this on prior versions because _______

⏯ Playground Link

https://www.typescriptlang.org/play/?#code/C4TwDgpgBAYg9nAPAFQHxQLxQN5QHYCGAthAFxQBEAZghVAL5QBkUyA3AFCiRQBCBAJxTosuAG4EANgFcyrBpy7hoyAJYATVRHXDM8iAA9gEPOoDOfQYlV4qEAVABq6APysNW9QH14SNFHJ2JR41TW0fBF0sZChDY1MLX2tbeydXeUDFKmk8AGNgVTg8KBo-VAAKXIAjQI9tYQBKHA4oKAEIYGkBYurOeg4OXKKzYDaIM2lJYABGPVLy3EIScmpaABooEjMzAgBzOQoACQgQAEI6RgJEyNwtnf3yEYEbXYZUBs52ianpgDoliBsKAAemBUAAogIBHABOQAArQyACUBQADkANRUHUcHG+Dgo0MqhGUCKUG40FRt3G9zkTxeQPoqIGQzwxK+k2AACY5ggFvhiAdShQNnc9gdjmcLh8OOyppz-gKgaCoAA5OCxKEwoA

💻 Code

type Foo<T> = { name: "foo" } & T;
type Bar<T> = { value: T };

type Tidied<T> = T extends Bar<infer V> ? Tidied_Foo<T> : T;
type Tidied_Foo<T> = T extends Foo<infer V> ? T : T;

function foo<T>(cb: Tidied<T>) {
  return cb;
}

const result1 = foo({ name: "foo", message: "Hey!" } as Foo<{ message: string }>);
result1.name; // Error: Property 'name' does not exist on type '{ message: string; }'

const result2 = foo({ name: "foo", message: "Hey!" });
result2.name; // No error

🙁 Actual behavior

packages/server/src/test.ts:13:9 - error TS2339: Property 'name' does not exist on type '{ message: string; }'.

13 result1.name; // ...
           ~~~~


Found 1 error in packages/server/src/test.ts:13

🙂 Expected behavior

no type error

Additional information about the issue

I was working on my project Cuple. I found a weird bug. I spent my day to provide a small minimal example. I have a type Tidied which is used to make a developer friendly type from the complex built response types. It worked well for some cases but it doesn't work for the case that I provided. The original version makes more sense.

@fxdave fxdave changed the title Inference trims types Conditional Type Inference Bug in TS 5.x May 14, 2024
@fxdave
Copy link
Author

fxdave commented May 20, 2024

I could reduce the code even more. I believe it's minimal now. If I reduce the code more the problem goes away.

@RyanCavanaugh
Copy link
Member

Bisects to #55941

@RyanCavanaugh
Copy link
Member

cc @Andarist - any thoughts? Should we revert that PR?

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label Jun 6, 2024
@RyanCavanaugh RyanCavanaugh added this to the TypeScript 5.6.0 milestone Jun 6, 2024
@Andarist
Copy link
Contributor

Andarist commented Jun 6, 2024

@RyanCavanaugh hm, I'm not sure if this bisect result is correct. The referenced PR has been included since 5.4.0-dev.20231201 but the reported change is between 4.9 and 5.0

@Andarist
Copy link
Contributor

Andarist commented Jun 6, 2024

This changed between 5.0.0-dev.20221103 and 5.0.0-dev.20221108 . Looking at the commits within that range, I think the change was caused by #51405

@RyanCavanaugh RyanCavanaugh assigned ahejlsberg and unassigned weswigham Jun 6, 2024
@Andarist
Copy link
Contributor

Andarist commented Jun 6, 2024

I can confirm that #51405 changed this and it looks like working by design to me.

Matching types are eliminated from inference here as per the comment close to that touched code:

// We reduce intersection types unless they're simple combinations of object types. For example,
// when inferring from 'string[] & { extra: any }' to 'string[] & T' we want to remove string[] and

This code tries to infer from Foo<{ message: string; }> to { name: "foo"; } & V & T. That source is an aliased { name: "foo"; } & { message: string; } so { name: "foo"; } gets matched in both source and the target and "eliminated". Then it proceed to infer between the leftovers:

source // { message: string; }
target // V & T

This call is meant to return T, so ye - T doesn't have .name here as that part of the original intersection was matched away.

@fxdave
Copy link
Author

fxdave commented Jun 6, 2024

@Andarist Thanks for the investigation. I would use the unsimplified version, but if Typescript is going that direction, I can live with it. Do you think we can show a warning so this case wouldn't look like unexpected?

@Andarist
Copy link
Contributor

Andarist commented Jun 6, 2024

Do you think we can show a warning so this case wouldn't look like unexpected?

Where you'd like to show a warning? Inference is incapable of raising warnings

@RyanCavanaugh RyanCavanaugh added Working as Intended The behavior described is the intended behavior; this is not a bug and removed Needs Investigation This issue needs a team member to investigate its status. labels Jun 6, 2024
@RyanCavanaugh RyanCavanaugh removed this from the TypeScript 5.6.0 milestone Jun 6, 2024
@RyanCavanaugh
Copy link
Member

@Andarist is right - this code isn't really distinguishable from code that is removing name on purpose

We can't warn every time any possible thing happens.

@fxdave
Copy link
Author

fxdave commented Jun 9, 2024

@RyanCavanaugh @Andarist
I've just checked the comments again and something is not clear to me.

As I see, my Tidied should be equivalent with this:
type Tidied<T> = T
because every case resolves to T.

Why would this mean that I want to remove a random property from the type?

I also tried this for curiosity (this is the same problem without functions):

type Infer<T> = T extends Tidied<infer V> ? Tidied<V> : null
type Baz = Infer<Foo<{ message: string }>>

This removes "name" from the type, however, not with type Tidied<T> = T.

So what's the difference between this:

type Tidied<T> = T extends Bar<infer V> ? Tidied_Foo<T> : T;
type Tidied_Foo<T> = T extends Foo<infer V> ? T : T;

and this:

type Tidied<T> = T

?

@Andarist
Copy link
Contributor

Andarist commented Jun 9, 2024

T is unknown at the beginning and TS tries to infer what it could be to satisfy your constraints. It's all heuristics here. Both { message: string } and Foo<{ message: string }> are valid answers here. So this can't be qualified as a bug - the current behavior just doesn't match your original expectations.

@typescript-bot
Copy link
Collaborator

This issue has been marked as "Working as Intended" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

@typescript-bot typescript-bot closed this as not planned Won't fix, can't repro, duplicate, stale Jun 12, 2024
@fxdave
Copy link
Author

fxdave commented Jun 12, 2024

@Andarist Thank you. I think I understand it now.

For example, because type Foo<A> = A is only A, A can be anything that satisfies B in this scenario:

type Infer<B> = B extends Foo<infer A> ? Foo<A> : null

So here, typescript resolves to a seemingly random but correct type through its heuristics.

Infer<{ bar: string }> = Foo<unknown> // ✅ correct
Infer<{ bar: string }> = Foo<{}> // ✅ correct
Infer<{ bar: string }> = Foo<{ bar: string }> // ✅ correct

Similarly if the type is little bit more complex, e.g.: type Foo<A> = { bar: A }
then again,A can be anything. We can be sure that the property bar is there but we cannot be sure about its type.
For example:

Infer<{ bar: { baz: string } }> = Foo<unknown>  // ✅ correct
Infer<{ bar: { baz: string } }> = Foo<{}> //  ✅ correct
Infer<{ bar: { baz: string } }> = Foo<{ baz: string }> //  ✅ correct
Infer<{ bar: { baz: string } }> = Foo<{ baz: string, answer: 42 }> //  ✅ correct, but unlikely

Am I right? And, in that case, when can I rely on it?

A more practical use case would be:

type ArrayItem<T> = T extends Array<infer V> ? V : never

I would expect that I get TValue from Array<TValue> always, but with the current version of Typescript I only get a subtype of TValue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

6 participants