You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
typeFoo<T>={name: "foo"}&T;typeBar<T>={value: T};typeTidied<T>=TextendsBar<infer V> ? Tidied_Foo<T> : T;typeTidied_Foo<T>=TextendsFoo<infer V> ? T : T;functionfoo<T>(cb: Tidied<T>){returncb;}constresult1=foo({name: "foo",message: "Hey!"}asFoo<{message: string}>);result1.name;// Error: Property 'name' does not exist on type '{ message: string; }'constresult2=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.
Ryan Cavanaugh (@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
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.
Mateusz Burzyński (@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?
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.
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:
Activity
[-]Inference trims types[/-][+]Conditional Type Inference Bug in TS 5.x[/+]fxdave commentedon 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 commentedon Jun 6, 2024
Bisects to #55941
RyanCavanaugh commentedon Jun 6, 2024
cc Mateusz Burzyński (@Andarist) - any thoughts? Should we revert that PR?
Andarist commentedon Jun 6, 2024
Ryan Cavanaugh (@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 commentedon 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
Andarist commentedon 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:
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:This call is meant to return
T, so ye -Tdoesn't have.namehere as that part of the original intersection was matched away.fxdave commentedon Jun 6, 2024
Mateusz Burzyński (@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 commentedon Jun 6, 2024
Where you'd like to show a warning? Inference is incapable of raising warnings
RyanCavanaugh commentedon Jun 6, 2024
Mateusz Burzyński (@Andarist) is right - this code isn't really distinguishable from code that is removing
nameon purposeWe can't warn every time any possible thing happens.
fxdave commentedon Jun 9, 2024
Ryan Cavanaugh (@RyanCavanaugh) Mateusz Burzyński (@Andarist)
I've just checked the comments again and something is not clear to me.
As I see, my
Tidiedshould be equivalent with this:type Tidied<T> = Tbecause 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):
This removes "name" from the type, however, not with
type Tidied<T> = T.So what's the difference between this:
and this:
?
Andarist commentedon Jun 9, 2024
Tis 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 }andFoo<{ 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 commentedon Jun 12, 2024
This issue has been marked as "Working as Intended" and has seen no recent activity. It has been automatically closed for house-keeping purposes.
fxdave commentedon Jun 12, 2024
Mateusz Burzyński (@Andarist) Thank you. I think I understand it now.
For example, because
type Foo<A> = Ais onlyA,Acan be anything that satisfiesBin this scenario:So here, typescript resolves to a seemingly random but correct type through its heuristics.
Similarly if the type is little bit more complex, e.g.:
type Foo<A> = { bar: A }then again,
Acan be anything. We can be sure that the propertybaris there but we cannot be sure about its type.For example:
Am I right? And, in that case, when can I rely on it?
A more practical use case would be:
I would expect that I get
TValuefromArray<TValue>always, but with the current version of Typescript I only get a subtype of TValue.