为什么 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 of
never`):
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
我在启用 strictNullChecks
的 TypeScript 3.6.2 中编译。
假设我声明了一个可能未定义的变量:
let filename: string|undefined;
然后,回调可以为其分配一个值,或者将其保留为未定义:
doIt(() => filename = "assigned");
现在我检查回调是否分配给 filename; otherwise,
filenameis undefined and I exit the program (return value of
never`):
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