TypeScript: Inconsistent narrowing in arrow function
TypeScript Version: 3.6.0-dev.20190704
Search Terms: inconsistent union narrow const initialization arrow function
Code
interface OpenState {
isOpen: true;
}
interface ClosedState {
isOpen: false;
}
type State = OpenState | ClosedState;
const state: State = { isOpen: false };
const arrow = () => state;
function fn() { return state }
class Foo {
method() { return state }
boundMethod = () => state
}
type Arrow = typeof arrow
type Fn = typeof fn
type Method = typeof Foo.prototype.method
type BoundMethod = typeof Foo.prototype.boundMethod
Expected behavior:
Arrow === Fn === Method === BoundMethod
What that type should be is #31734
Actual behavior:
Arrow === () => ClosedState
Fn === Method === BoundMethod === () => State
Playground Link: Playground
Related Issues: #31734, #8513, possibly dupe of #29260 but I don’t think so
A couple things to highlight:
- Changing
const statetolet stateachieves the expected behavior with a value ofState. - Hovering over
const stateindicates that it isState, even thoughtypeof state === ClosedState. I did not create a separate issue for that because it seems likely that that is a symptom of this issue rather than a distinct issue with the language server.
About this issue
- Original URL
- State: closed
- Created 5 years ago
- Comments: 17 (6 by maintainers)
The root cause is that control flow narrowing applies to arrows and function expressions. It’s not unique to arrows:
Now
typeof fexpr === typeof arrow.The reason is that function and class declarations are hoisted – so control flow analysis doesn’t apply because the compiler isn’t sure when they will run. They use the declared type of
state: State. However,() => stateis not hoisted, and (optimistically) captures the narrowed type ofstate: ClosedStateat the point that it is captured.People write code like this and expect it to work:
It’d be really weird to have
constbehave worse thanletin this regard, since you definitely want narrowing based on assignments tolets. It’d also be a new and worse inconsistency to have initializations be ignored but not assignments.