假设我们有这个函数:
function returnNever(): never {
throw new Error();
}
创建 IIFE 时,其后面的代码将被标记为无法访问:
(async () => {
let b: string;
let a0 = returnNever();
b = ""; // Unreachable
b.toUpperCase(); // Unreachable
})();
这按预期工作。请注意,
a0
被推断为 never
类型。
但是,如果
returnNever()
返回 Promise<never>
并等待,则行为会有所不同:
(async () => {
let b: string;
let a1 = await Promise.reject(); // returns Promise<never>
b = ""; // Not unreachable?
b.toUpperCase(); // Not unreachable?
})();
在这种情况下,
a1
也被推断为never
类型。但之后的代码并没有被标记为无法访问。为什么?
背景: 我最近偶然发现了一些
logError
函数,如下代码所示。它在 catch
块内使用。这样,我发现,不仅可达性分析,而且明确的分配分析也受其影响:
declare function fetchB(): Promise<string>;
async function logError(err: any): Promise<never> {
await fetch("/foo/...");
throw new Error(err);
}
(async () => {
let b: string;
try {
b = await fetchB(); // Promise<string>
} catch (err) {
await logError(err); // awaiting Promise<never>
}
b.toUpperCase(); // Error: "b" is used before assignment
})();
如果
logError
同步(通过删除所有与 await
相关的 async
和 logError
),则不会出现错误。另外,如果 let b: string
更改为 let b: string | undefined
,则在 try-catch 块之后 undefined
不会被删除。
似乎有理由在控制流分析的任何方面都不考虑
await
的 Promise<never>
返回函数。
这也可能是一个错误,但我宁愿认为我在这里遗漏了一些细节。
A
**Promise<never>**
被视为可能引发任何错误(包括运行时错误)的类型。因为运行时有可能会抛出错误,所以await之后的代码被认为是可达的。
为了防止意外的运行时错误,TypeScript 对
Promise<never>
类型的行为旨在更加自由和保守。尽管有时不合逻辑,但这是一个经过深思熟虑的选择,旨在解决承诺和潜在的运行时异常带来的不确定性,同时保持类型系统的健全性。
这也可能是一个错误,但我宁愿认为我在这里遗漏了一些细节。
这确实是一个错误,有关它的 GitHub 问题(由该问题的作者报告)可以在 here 找到。截至2023年,该问题仍未解决。