我正在启用strictNullChecks
的TypeScript 3.6.2中进行编译。
说我声明了一个可能未定义的变量:
let filename: string|undefined;
然后,一个回调may为它分配一个值,或不定义它:
doIt(() => filename = "assigned");
现在,我检查是否已将回调分配给filename; otherwise,
文件名is undefined and I exit the program (return value of
从不:):
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
返回,这意味着程序在到达使用filename
的以下行之前就终止了;因此,filename
必须是字符串!我在这里想念什么吗?为什么TypeScript仍然相信filename
在永不返回后仍可能是未定义的?
为了复制,这是完整的程序:
let filename: string|undefined;
doIt(() => filename = "assigned");
if (filename === undefined) {
process.exit(0);
}
console.log(filename.toUpperCase());
function doIt(fn: () => void) {
fn();
}
注:由于可以初始化filename = ""
,并可以在if语句中进行检查,因此可以在实际程序中解决问题。但是,我想知道为什么此特定方法不起作用。
EDIT:这是我的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"
]
}
这是打字稿3.6.3和更早版本中的行为,但实际上它按照您在3.7.2版中所希望的方式工作;这是一个Playground Link供您自己查看。如果使用菜单在版本之间来回切换,则错误会出现并消失。
如果您的项目需要这样做,则可以升级Typescript。
[基本上,问题在于控制流图是在类型检查之前确定的,因此在形成CFG(并检查可达性)时,exit
返回never
的事实不可用,因此,调用exit
的CFG分支继续到if
语句之后的代码,其中变量处于可能未定义的状态。
这是2016年12月的raised as an issue on GitHub,根据a response in a different thread,
#12825概括处理永不退货
- 控制流程图是在绑定期间形成的,但是我们还没有类型数据
- 我们可以将所有呼叫存储在每个流控制点,然后检查它们是否永不返回,并检查此信息以了解计算类型
- 昂贵!
- 正确的分析将需要多次迭代
因此,这是在3.6.3和更早版本中可能未解决的一些原因。
因此,这里的主要问题是在3.6版中,永不返回的函数并未参与控制流分析。此PR在3.7中实现了此功能。
我运行您的代码(复制了节点定义中的某些类型,我们看到它可以在3.7中工作,但在3.6中不工作
此外,设置变量的箭头功能实际上与结果无关。 Typescript不会对如何从doIt
调用回调进行任何控制流分析。在this问题中对此进行了详细说明。
仅注意3.7,并且永远不要重新调整功能,它们参与控制流的条件非常严格:
在以下情况下,将函数调用分析为断言调用或永不返回的调用>
- 该调用作为顶级表达式语句发生,并且
- 该调用为函数名称指定一个标识符或标识符的点缀序列,并且
- 函数名称中的每个标识符引用具有显式类型的实体,并且
- 函数名称解析为具有断言返回类型或显式永不返回类型注释的函数类型。
当一个实体被声明为函数,方法,类或名称空间,或者被声明为带有显式类型注释的变量,参数或属性时,它被认为具有显式类型。 (存在此特定规则,以便对潜在断言调用的控制流分析不会循环触发进一步的分析。)
因此,如果函数表达式没有显式注释,则它可能不参与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