Incorrect narrowing for nested tag unions
See original GitHub issueBug Report
🔎 Search Terms
“nested union narrowing”, “nested tag union”
🕗 Version & Regression Information
This is the behavior in every version I tried, and I reviewed the FAQ.
⏯ Code
type Config =
| { type: 'text'; value: string }
| { type: 'number'; value: number }
| { type: 'window' | 'section'; children: Config[] }
;
function foo(config: Config) {
if (config.type === 'window' || config.type === 'section') {
return '';
}
return config.value; // <-- Property 'value' does not exist on type '{ type: "window" | "section"; children: Config[]; }'.
}
type Config2 =
| { type: 'text'; value: string }
| { type: 'number'; value: number }
| { type: 'window'; children: Config[] }
| { type: 'section'; children: Config[] }
;
function foo2(config: Config2) {
if (config.type === 'window' || config.type === 'section') {
return '';
}
return config.value; // <-- OK
}
Output
"use strict";
function foo(config) {
if (config.type === 'window' || config.type === 'section') {
return '';
}
return config.value;
}
function foo2(config) {
if (config.type === 'window' || config.type === 'section') {
return '';
}
return config.value;
}
Compiler Options
{
"compilerOptions": {
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true,
"strictBindCallApply": true,
"noImplicitThis": true,
"noImplicitReturns": true,
"alwaysStrict": true,
"esModuleInterop": true,
"declaration": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"target": "ES2017",
"jsx": "react",
"module": "ESNext",
"moduleResolution": "node"
}
}
Playground Link: Provided
🙁 Actual behavior
Getting an error in foo
: Property 'value' does not exist on type '{ type: "window" | "section"; children: Config[]; }'.
This is strange because changing config.value
to config.type
on that last line of foo
and hovering over it shows that it was correctly narrowed to "number" | "text"
. However, the config.value
access shows the error above that suggests that config
was narrowed to { type: "window" | "section"; children: Config[]; }
instead, even though it’s the branch that was supposed to be discarded by if
.
🙂 Expected behavior
foo
to compile successfully, just like foo2
.
Issue Analytics
- State:
- Created 2 years ago
- Comments:5 (4 by maintainers)
Top Results From Across the Web
Type narrowing of "one level deep" discriminated union in ...
I believe that there is an issue in TS , because it is unable to infer nested properties of unions. Here is the...
Read more >narrowing types via type guards and assertion functions - 2ality
The problem in this case is that, without narrowing, we can't access property .second of a value whose type is FirstOrSecond . The...
Read more >Documentation - Narrowing - TypeScript
The in operator narrowing JavaScript has an operator for determining if an object has a property with a name: the in operator. TypeScript...
Read more >C++ Core Guidelines - GitHub Pages
tag is the anchor name of the item where the Enforcement rule appears ... unions; casts; array decay; range errors; narrowing conversions ......
Read more >Modeling with Union Types - Thoughtbot
Tagged unions are a killer language feature as they allow you to expressively model problem domains and avoid some of the pitfalls of ......
Read more >Top Related Medium Post
No results found
Top Related StackOverflow Question
No results found
Troubleshoot Live Code
Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start FreeTop Related Reddit Thread
No results found
Top Related Hackernoon Post
No results found
Top Related Tweet
No results found
Top Related Dev.to Post
No results found
Top Related Hashnode Post
No results found
Top GitHub Comments
It actually does say “on type
Config
”–the subsequent message is simply elaboration telling you which constituent ofConfig
caused the property access to fail and has nothing to do with the attempted narrowing. The fact that it matches the type you were trying to narrow away inside the condition is just a coincidence.Whenever you see multiple lines for a type error, you can mentally prepend “because” to every line after the first.
The problem here is that narrowing proceeds step-by-step, and after determining
config.type === 'window'
to be false, we can’t narrow the union at all – its original three constituents are still around. If there was a single operation to excludewindow
andsection
as an atomic step, then the code would work as expected:We’d need negated types to form a new type,
Config & { type: not 'window' }
at the||
point in order to make this work.