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
{{ message }}
Repository navigation
Type guard affects type of variable in surprising wayΒ #50916
typeIdentity<T>={[KinkeyofT]: T[K]};typeSelf<T>=Textendsunknown ? Identity<T> : never;functionis<T>(value: T): value is Self<T>{returntrue;}typeUnion={a: number}|{b: number}|{c: number};functionexample(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)){}returnx;// ^?}
π 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
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:
Note that one issue with favoring the asserted type is that CFA continues with that type after the conditional block. Though undesired, that's an effect of how CFA works in the face of mutual subtypes. This behavior was already present for singleton types, but now also extends to union types.
So this is effectively a design limitation, but we'll continue to think of ways in which to improve it.
Would it be possible to have TS more eagerly resolve Identity<Identity<Identity<Identity<Identity<... down to Union? A problem that we encountered is that given a more complex type and enough if 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.
The core issue is that Union and Identity<Union> are both subtypes of each other. If you change Identity<T> to always be a subtype of T, 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?
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 either T or a subtype of T, and therefore types property revert back as expected.
exporttypeRequire<T,KextendskeyofT>=Pick<T,K>extendsRequired<Pick<T,K>> ? T : Omit<T,K>&Required<Pick<T,K>>;
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 either T or a subtype of T, and therefore types property revert back as expected.
exporttypeRequire<T,KextendskeyofT>=Pick<T,K>extendsRequired<Pick<T,K>> ? T : Omit<T,K>&Required<Pick<T,K>>;
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.
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 enoughifblocks, 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
UnionandIdentity<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 eitherTor 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.
MichaelMitchell-at commentedon Feb 14, 2023
Thanks Anders Hejlsberg (@ahejlsberg) !