Closed
Description
Bug Report
π Search Terms
type guard / fall through / narrow / change / lazy / evaluate
π Version & Regression Information
- This changed between versions v4.7.4 and v4.8.2
β― Playground Link
Playground link with relevant code
π» Code
type Identity<T> = {[K in keyof T]: T[K]};
type Self<T> = T extends unknown ? Identity<T> : never;
function is<T>(value: T): value is Self<T> {
return true;
}
type Union = {a: number} | {b: number} | {c: number};
function example(x: Union) {
if (is(x)) {}
if (is(x)) {}
if (is(x)) {}
if (is(x)) {}
if (is(x)) {}
if (is(x)) {}
if (is(x)) {}
if (is(x)) {}
return x;
// ^?
}
π Actual behavior
The type of x
is "narrowed" to Identity<Identity<Identity<Identity<Identity<Identity<Identity<Identity<{a: number;}>>>>>>>> | Identity<Identity<Identity<Identity<Identity<Identity<Identity<Identity<{b: number;}>>>>>>>> | Identity<Identity<Identity<Identity<Identity<Identity<Identity<Identity<{c: number;}>>>>>>>>
It's as if a variable gets narrowed to the union of the types of both sides of the type predicate, e.g.
if (isA(aOrB)) {
// `aOrB` gets narrowed to `A`
} else {
// `aOrB` gets narrowed to `Exclude<typeof aOrB, A>`
}
// `aOrB` gets narrowed to `A | Exclude<typeof aOrB, A>` but it should just be left alone
π Expected behavior
The type of x
doesn't change.
Activity
ahejlsberg commentedon Sep 23, 2022
This is an effect of #50044. The issue here is that the argument type and the asserted type are subtypes of each other, and therefore appear interchangeable in control flow analysis. From the PR:
So this is effectively a design limitation, but we'll continue to think of ways in which to improve it.
MichaelMitchell-at commentedon Sep 23, 2022
Would it be possible to have TS more eagerly resolve
Identity<Identity<Identity<Identity<Identity<...
down toUnion
? A problem that we encountered is that given a more complex type and enoughif
blocks, the narrowed type becomes complex enough that it both slows down tsserver to a crawl and we start getting "type instantiation is excessively deep" errors.ahejlsberg commentedon Sep 23, 2022
The core issue is that
Union
andIdentity<Union>
are both subtypes of each other. If you changeIdentity<T>
to always be a subtype ofT
, for example by intersecting with a tag type, things work as expected in your scenario:Fundamentally, when the argument and asserted types are subtypes of each other, the reason we favor the asserted type is that you must have written the assertion for some reason. I'm not sure what the reason is in your example, but presumably there is some difference in behavior?
MichaelMitchell-at commentedon Sep 23, 2022
Here's an example that is a lot closer (though still simplified) to the actual situation that we had:
Playground Link
ahejlsberg commentedon Sep 27, 2022
I that last example, I'd recommend re-writing the
Require<T, K>
type to only narrow when necessary. This way the returned type is eitherT
or a subtype ofT
, and therefore types property revert back as expected.MichaelMitchell-at commentedon Sep 27, 2022
That's what we pretty much ended up doing (the playground link in the description actually has an example at the bottom of the code) and we added this optimization to our other utility types, but it was certainly an unexpected footgun as this was the first time we've ever had to optimize our types in this particular way.
ahejlsberg commentedon Sep 27, 2022
Agreed about the footgun. We'll continue to think about ways to improve this.
strictSubtypeRelation
andgetNarrowedType
#52282MichaelMitchell-at commentedon Feb 14, 2023
Thanks @ahejlsberg !