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:

  1. Changing const state to let state achieves the expected behavior with a value of State.
  2. Hovering over const state indicates that it is State, even though typeof 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)

Most upvoted comments

The root cause is that control flow narrowing applies to arrows and function expressions. It’s not unique to arrows:

const fexpr = function () { return state }

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, () => state is not hoisted, and (optimistically) captures the narrowed type of state: ClosedState at the point that it is captured.

People write code like this and expect it to work:

type Method = "GET" | "POST" | "PATCH";
declare function doSomething(x: "GET" | "POST") { ... }

// later
const x: Method = "GET";
doSomething(x);

It’d be really weird to have const behave worse than let in this regard, since you definitely want narrowing based on assignments to lets. It’d also be a new and worse inconsistency to have initializations be ignored but not assignments.