Repository navigation
Closed as not planned
Labels
Working as IntendedThe behavior described is the intended behavior; this is not a bugThe behavior described is the intended behavior; this is not a bug
Description
🔎 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
💻 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.
Metadata
Metadata
Assignees
Labels
Working as IntendedThe behavior described is the intended behavior; this is not a bugThe behavior described is the intended behavior; this is not a bug
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.