为什么 TypeScript 不能确定在检查未定义 returns 的 if 语句之后定义了一个变量 never

Why can TypeScript not determine that a variable is defined after an if statement that checks for undefined returns never

我在启用 strictNullChecks 的 TypeScript 3.6.2 中编译。

假设我声明了一个可能未定义的变量:

let filename: string|undefined;

然后,回调可以为其分配一个值,或者将其保留为未定义:

doIt(() => filename = "assigned");

现在我检查回调是否分配给 filename; otherwise,filenameis undefined and I exit the program (return value ofnever`):

if (filename === undefined) {
  process.exit(0);
}

如果这个 if 条件为假,那意味着 filename 必须 有一个有效的字符串值,对吗?最后,我尝试使用我最确定的字符串:

console.log(filename.toUpperCase());

但是,我得到一个错误:

source/repro.ts:6:13 - error TS2532: Object is possibly 'undefined'.

6 console.log(filename.toUpperCase());
              ~~~~~~~~


Found 1 error.

据我所知,由于上面的 if 语句有一个 never return,这意味着程序在到达以下使用 [= 的行之前终止22=];因此,filename 必须是一个字符串!我在这里错过了什么吗?为什么 TypeScript 仍然相信它 filename 在 never return 之后仍然是未定义的?


为了复制,这里是完整的程序:

let filename: string|undefined;
doIt(() => filename = "assigned");
if (filename === undefined) {
  process.exit(0);
}
console.log(filename.toUpperCase());
function doIt(fn: () => void) {
  fn();
}

注意:我可以在我的真实程序中解决我的问题,因为我可以初始化 filename = "",并在 if 语句中检查它。但是,我想知道为什么 这种特定方法 行不通。


编辑:这是我的 tsconfig.json。我在一个没有 tsconfig.json 的全新文件夹中尝试了这个例子,但我无法重现这个错误。也许我的 tsconfig 中有一些东西,但我还没有确定它:

{
  "compilerOptions": {
    "module": "commonjs",
    "target": "es2017",
    "moduleResolution": "node",
    "sourceMap": true,
    "outDir": "dist",
    "declaration": true,
    "alwaysStrict": true,
    "noImplicitThis": true,
    "noImplicitReturns": true,
    "noImplicitAny": true,
    "strictBindCallApply": true,
    "strictFunctionTypes": true,
    "strictNullChecks": true,
    "noUnusedLocals": true
  },
  "include": [
    "source/**/*.ts"
  ]
}

这是 Typescript 3.6.3 及更早版本中的行为,但它实际上在 3.7.2 版本中按照您希望的方式工作;这里有一个 Playground Link 供您自己查看。如果您使用菜单在版本之间来回切换,错误会出现并消失。

如果您的项目需要这样做,那么您可以升级 Typescript。


基本上,问题在于控制流图是在类型检查之前确定的,因此在形成 CFG(并检查可达性)时,exit returns never 不可用,因此调用 exit 的 CFG 分支继续执行 if 语句之后的代码,其中变量处于可能未定义的状态。

这是 raised as an issue on GitHub in December 2016, and according to a response in a different thread,

#12825 Generalize handling of never for returns

  • The control flow graph is formed during binding, but we don't have type data yet
  • We could store all calls at each flow control point and then check them for never returns and check this info for computing types
    • Expensive!
  • Correct analysis would require multiple iterations

因此,这些是在 3.6.3 及更早版本中可能未解决的一些原因。

所以这里的主要问题是在 3.6 中永不返回的函数没有参与控制流分析。此功能在 3.7 中由 PR 实现。

你 运行 你的代码(复制了节点定义中的一些类型)我们可以看到它可以在 3.7 but not in 3.6

中工作

此外,设置变量的箭头函数实际上与结果无关。 Typescript 不会对如何从 doIt 调用回调进行任何控制流分析。这在this问题中有详细说明。

只管3.7不重调函数,参与控制流的条件很严格:

A function call is analyzed as an assertion call or never-returning call when

  • the call occurs as a top-level expression statement, and
  • the call specifies a single identifier or a dotted sequence of identifiers for the function name, and
  • each identifier in the function name references an entity with an explicit type, and
  • the function name resolves to a function type with an asserts return type or an explicit never return type annotation.

An entity is considered to have an explicit type when it is declared as a function, method, class or namespace, or as a variable, parameter or property with an explicit type annotation. (This particular rule exists so that control flow analysis of potential assertion calls doesn't circularly trigger further analysis.)

因此,如果函数表达式没有显式注释,它可能不会参与 CFA

const exit = () =>  {
  throw new Error()
}

let filename: string | undefined;
if (filename === undefined) {
  exit();
}
console.log(filename.toUpperCase()); // error 

Playground Link 使用显式注释它可以工作:

const exit: () => never = () =>  {
  throw new Error()
}

let filename: string | undefined;
if (filename === undefined) {
  exit();
}
console.log(filename.toUpperCase()); // error 

Playground Link