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
Not sure if "recursive" is the right word to apply to this situation. A discriminant property only applies to the object it's directly a member of. So in your example, inside the if block, you can access x.type.a (but not x.type.b), but there are no effects on the containing object x.
Reacted by Christian Farías Águila and Anoesj Sadraee
I have run across something similar a few times, where having to peel off/unwrap properties (DOM events come to mind) and operate/assert on different parts and pieces.
It does seem to be a similar problem space to #11117, where the value of one property effects the type of another, though obviously with the above the compiler could already deduce that without any additional syntax, which #11117 would likely require.
I've been doing some digging in src/compiler/checker.ts and I guess the fix should be somewhere around here:
functionisMatchingReference(source: Node,target: Node): boolean{switch(source.kind){caseSyntaxKind.Identifier:
returntarget.kind===SyntaxKind.Identifier&&getResolvedSymbol(<Identifier>source)===getResolvedSymbol(<Identifier>target)||(target.kind===SyntaxKind.VariableDeclaration||target.kind===SyntaxKind.BindingElement)&&getExportSymbolOfValueSymbolIfExported(getResolvedSymbol(<Identifier>source))===getSymbolOfNode(target);// || (target.kind === SyntaxKind.PropertyAccessExpression && whaever else that needs to be checked here)
Of course given the complexity of this file (createTypeChecker is a single function with ~30K lines of code), I'm not sure what the impact of this change is and what else could break from making the change.
Anyway, hope this helps, I'd really like to have this working.
Where that type is identical to the nesteddiff.type field
I'm finding writing typescript with lots of these unions to be very pleasant, this would make that process simpler and more effective. Thanks for adding these ADT features to begin with!
I ended up squishing my types all down to the same level to avoid these issues. One issue with this approach is the field-name collisions, which makes it feel less composable, but if every field in all the objects have unique names then it's probably just as composable.
I haven't thought about the nesting enough to know all the implications.
I just wanted to post this so you see how the issue comes up in the real world and one possible way of people fixing it so you see what the current version of typescript is encouraging. There may be some negatives as well to allowing this nesting of unions, I'm not sure, I know in elm-lang they try to avoid nesting in general for various reasons. But if we don't have the nesting it seems that we need a way to deal with the name collision problem so we can & things together.
Even if this worked only with an object spread, that would help in some unambiguously valid cases:
typeA={b: 'c'|'d';};typeB={b: 'c'};conste=(a: A)=>{if(a.b==='c'){constf: B={ ...a};// Error: Type '{ b: "c" | "d"; }' is not assignable to type 'B'.}};
+1 here. I debugged this for a bit to realize that unions can only be discriminated at the top level / direct. I'm also copying fields down the chain for this purpose
Reacted by Eddie Xie, Aaron Adams, Jakub Riedl, Zuozhi Wang, Denis Zhelnerovich, jacobgmathew and Dawson Booth
+1. Just ran into this. I'm trying to type an object that's not in my control (comes from a third-party API response), which looks as follows (compiles correctly, but doesn't behave as expected):
Edit: For others who might stumble across this: I was able to achieve what I wanted with User Defined Type Guards. Needed a lot of boilerplate (thanks to copilot for helping out), but at least got the job done.
typeSettingType<Nameextendsstring,Settings>={type: {name: Name}settings: Settings};typeSettingFoo=SettingType<'foo',{a: boolean;b: string}>typeSettingBar=SettingType<'bar',{c: string[]}>typeSettingBaz=SettingType<'baz',{d: number[]}>typeSettings=|SettingFoo|SettingBar|SettingBaz;constisSettingFoo=(s: Settings): s is SettingFoo=>s.type.name==='foo';constisSettingBar=(s: Settings): s is SettingBar=>s.type.name==='bar';constisSettingBaz=(s: Settings): s is SettingBaz=>s.type.name==='baz';
👋 currently there is a way (with some heavy type foo) to actually get this to work, I'll use Rakesh Pai (@rakeshpai) example, but others should work as well:
// The nested tagged union typetypeX=|{type: {name: 'foo'};settings: {a: boolean;b: string}}|{type: {name: 'bar'};settings: {c: string[]}}|{type: {name: 'baz'};settings: {d: number[]}};// ========================================================================== //// The type-level function that, given a name, returns the type(s) within the union for that name.// The second argument is required to get "distributive conditional types" to work (more on this after// this code snippet)typeGetTypeForName<TName,TX=X>=TXextends{type: {name: TName}}
? TX
: never;// Type predicate to run the narrowing in the outer object based on a nested discriminantconstis=<TNameextendsX['type']['name']>(x: X,name: TName,): x is GetTypeForName<TName>=>x.type.name===name;// ========================================================================== //// The function you actually wanted to write, all of the above is write-once boilerplateconsttest=(x: X)=>{// What you would like out of the box:// if (x.type.name === 'baz') console.log(x.settings.d);// What you can get, for now:if(is(x,'baz'))console.log(x.settings.d);};
You can read more about distributive conditional types here.
type Int = {
settings: {
fieldRule: 'Int'
}
selections: {
integer: number
}
}
type Dec = {
settings: {
fieldRule: 'Dec'
}
selections: {
decimal: number
}
}
function f(val: Int | Dec) {
if (val.settings.fieldRule === 'Int') {
console.log(val.selections.integer) // <-- error TS2339: Property 'integer' does not exist on type '{ integer: number; } | { decimal: number; }'. Property 'integer' does not exist on type '{ decimal: number; }'.
}
}
typeA={type: {name: "A"}a: number}typeB={type: {name: "B"}b: number}declareconstaOrB: A|Bif(aOrB.type.name==="A"){// implicitly means that `aOrB` is `{ type: { name: "A" }}`console.log(aOrB.a)// Error}functionhasTypeName<Nameextendsstring>(a: {type: {name: string}},name: Name): a is {type: {name: Name}}{returna.type.name===name}if(hasTypeName(aOrB,"A")){// explicitly means that `aOrB` is `{ type: { name: "A" }}`console.log(aOrB.a)// Works}
Reacted by TheYarin, Lev Chelyadinov, wiese, Albert Marashi, Simon and Mariusz `shd` Gliwiński
As I understand it, this issue is a subset of #42384. However while propagating the inference to the parent seems very complex to implement, being able to use a nested property as a disciminator seems quite a bit simpler while covering most (?) usecases.
I'd love this to be possible without a dedicated function. Even if I'd have to use dedicated syntax for this - e.g. something like this - it would go a long way:
// FAKE CODE, DO NOT USEclassBaseShape<Textendsstring>{publicinternal: {kind: T}// ...}classCircleextendsBaseShape<"circle">{publicradius: number=10;}classSquareextendsBaseShape<"square">{publicx: number=10;}classTriamgleextendsBaseShape<"triangle">{publicx: number=10;publicy: number=10;}typeShape=DeepInfer<Circle|Square|Triamgle>;// ortypeShape=DeepInfer<Circle|Square|Triamgle,'internal.kind'>;// User code:if(a.internal.kind==="circle"){console.log('radius is: ',a.radius);// ts should look for nested properties to infer type of the union and assume only Circle can fit}
Why does this matter to me? I am working on a SDK, that has typegen built in (kinda like ORM for web api). Search results of the sdk can be of any type, but each has dedicated type name described in internal object property. I can take extra code generation into typegen for recursive infering as long as user doesn't have to do anything in their code.
I don't know if performance is one of the reasons deeper nested discriminators having an effect on levels above in an object tree hasn't been realized as of yet. I'm guessing yes? If so, would tsgo's release pave the way to explore this again?
Activity
RyanCavanaugh commentedon Sep 26, 2017
This isn't currently how narrowing works, but we could consider expanding its rules to cover this case
ccorcos commentedon Sep 26, 2017
Hmm. I'm surprised. So its non-recursive?
RyanCavanaugh commentedon Sep 26, 2017
Not sure if "recursive" is the right word to apply to this situation. A discriminant property only applies to the object it's directly a member of. So in your example, inside the
ifblock, you can accessx.type.a(but notx.type.b), but there are no effects on the containing objectx.ccorcos commentedon Sep 26, 2017
yeah, but given my type definition that seems incomplete, doesnt it?
RyanCavanaugh commentedon Sep 26, 2017
I agree
kitsonk commentedon Sep 27, 2017
I have run across something similar a few times, where having to peel off/unwrap properties (DOM events come to mind) and operate/assert on different parts and pieces.
It does seem to be a similar problem space to #11117, where the value of one property effects the type of another, though obviously with the above the compiler could already deduce that without any additional syntax, which #11117 would likely require.
pzavolinsky commentedon Jul 22, 2018
I've been doing some digging in
src/compiler/checker.tsand I guess the fix should be somewhere around here:Of course given the complexity of this file (
createTypeCheckeris a single function with ~30K lines of code), I'm not sure what the impact of this change is and what else could break from making the change.Anyway, hope this helps, I'd really like to have this working.
Cheers!
AnyhowStep commentedon Sep 11, 2018
I was about to create a suggestion along the lines of "Narrow Tagged Union Recursively".
With the following examples,
A contrived example,
Less contrived,
amilner42 commentedon Feb 6, 2019
I currently am duplicating my tags to have them at every level.
Instead of:
I have to do:
Where that
typeis identical to the nesteddiff.typefieldI'm finding writing typescript with lots of these unions to be very pleasant, this would make that process simpler and more effective. Thanks for adding these ADT features to begin with!
Minor type renaming
amilner42 commentedon Feb 6, 2019
I ended up squishing my types all down to the same level to avoid these issues. One issue with this approach is the field-name collisions, which makes it feel less composable, but if every field in all the objects have unique names then it's probably just as composable.
Like:
I haven't thought about the nesting enough to know all the implications.
I just wanted to post this so you see how the issue comes up in the real world and one possible way of people fixing it so you see what the current version of typescript is encouraging. There may be some negatives as well to allowing this nesting of unions, I'm not sure, I know in elm-lang they try to avoid nesting in general for various reasons. But if we don't have the nesting it seems that we need a way to deal with the name collision problem so we can
&things together.pzavolinsky commentedon Feb 7, 2019
Unfortunately in my scenario I'm typing messages whose structure is beyond my control (i.e. they are produced by a different app).
I'd really like to have support for nested discriminant props to avoid having to have some monstrosity like:
That is used like:
When this could've been as simple as:
or even:
jeremybparagon commentedon Apr 3, 2019
Even if this worked only with an object spread, that would help in some unambiguously valid cases:
https://www.typescriptlang.org/play/#src=type%20A%20%3D%20%7B%20b%3A%20'c'%20%7C%20'd'%3B%20%7D%3B%0D%0Atype%20B%20%3D%20%7B%20b%3A%20'c'%20%7D%3B%0D%0Aconst%20e%20%3D%0D%0A%20%20%20%20(a%3A%20A)%20%3D%3E%20%7B%0D%0A%20%20%20%20%20%20%20%20if%20(a.b%20%3D%3D%3D%20'c')%20%7B%0D%0A%20%20%20%20%20%20%20%20%20%20%20%20const%20f%3A%20B%20%3D%20%7B%20...a%20%7D%3B%20%2F%2F%20Error%3A%20Type%20'%7B%20b%3A%20%22c%22%20%7C%20%22d%22%3B%20%7D'%20is%20not%20assignable%20to%20type%20'B'.%0D%0A%20%20%20%20%20%20%20%20%7D%0D%0A%20%20%20%20%7D%3B%0D%0A
3 remaining items
marcandrews commentedon Jul 29, 2019
I am encountering this issue with a Redux reducer with nesting
switchstatements 😢marcandrews commentedon Jul 29, 2019
Here is an example on TypeScript Playground. Please refer to lines 43 and 46.
kevinluvian commentedon Aug 27, 2019
any updates?
tejasmanohar commentedon Nov 1, 2019
+1 here. I debugged this for a bit to realize that unions can only be discriminated at the top level / direct. I'm also copying fields down the chain for this purpose
ShuiRuTian commentedon Feb 14, 2021
Hi, folks, if you are interested in this issue, could you do me a favour? Help check the test cases added in PR #38839.
For each test file(end with .ts), it would generate
.types,.symbolsanderrors.txtfile. I think.typesfile is the only file need to check.I have found some cases are not correct(pointed out similar case by TS team member), but it is easy to miss the error.
rakeshpai commentedon Dec 7, 2021
+1. Just ran into this. I'm trying to type an object that's not in my control (comes from a third-party API response), which looks as follows (compiles correctly, but doesn't behave as expected):
Would love to express this correctly.
Edit: For others who might stumble across this: I was able to achieve what I wanted with User Defined Type Guards. Needed a lot of boilerplate (thanks to copilot for helping out), but at least got the job done.
pzavolinsky commentedon Jul 1, 2022
👋 currently there is a way (with some heavy type foo) to actually get this to work, I'll use Rakesh Pai (@rakeshpai) example, but others should work as well:
You can read more about distributive conditional types here.
Also, I covered some of the building blocks for this solution ☝️ in this fan fiction story about typescript metaprogramming.
Hope this helps!
Threnos commentedon Dec 24, 2022
Just adding my case to this.
taralx commentedon Jan 16, 2023
FWIW, it is possible to chain up the narrowing with some silly-looking code:
ericbf commentedon Mar 7, 2023
Adding my 2¢ here. Explicit type guards work fine to discriminate by a sub property. It would be great if implicit type guards did as well.
Playground
crazyair commentedon Aug 3, 2023
Can it only support
(string | number)[]MichalMarsalek commentedon Aug 28, 2024
As I understand it, this issue is a subset of #42384. However while propagating the inference to the parent seems very complex to implement, being able to use a nested property as a disciminator seems quite a bit simpler while covering most (?) usecases.
AlbertMarashi commentedon Oct 4, 2024
This is incredibly annoying and frustrating :(
My simplified issue was:
The only work around is to basically do
which is a bit more unsafe
rgembalik commentedon Dec 12, 2024
I'd love this to be possible without a dedicated function. Even if I'd have to use dedicated syntax for this - e.g. something like this - it would go a long way:
Why does this matter to me? I am working on a SDK, that has typegen built in (kinda like ORM for web api). Search results of the sdk can be of any type, but each has dedicated type name described in
internalobject property. I can take extra code generation into typegen for recursive infering as long as user doesn't have to do anything in their code.Anoesj commentedon Jun 22, 2025
I don't know if performance is one of the reasons deeper nested discriminators having an effect on levels above in an object tree hasn't been realized as of yet. I'm guessing yes? If so, would tsgo's release pave the way to explore this again?