Typescript - 推断出 'never' 的错误推断

10

这是一个基本用例:将变量初始化为null,然后在某些嵌套的循环/函数中更改其值:

let a: number | null = null;
[1].forEach(() => {
  a = 1;
});

if (a != null)
  a.toFixed(); // Error: Property 'toFixed' does not exist on type 'never'.

然而,TypeScript 推断 a 的类型为 never。如果没有 if,我会认为它会将其推断为null | number,在这种情况下,我可能会得到一个错误,说明在 null 上不存在属性,但是仅根据初始赋值为什么它假定为 never 呢?
我做错了什么吗?

2
这意味着TypeScript在运行时无法准确了解forEach的功能。此外,请记住,forEach可以被修改以执行不同的操作,因此训练TypeScript了解它是毫无意义的。 - Andrei Tătar
TS 不会分析回调函数内部发生的事情。因为很难知道回调函数何时被调用(它们可能根本不会被调用)。 - Nurbol Alpysbayev
2个回答

7
如果您确定 a 在那里有一个值,那么您可以在变量后面加上 !
let a: number | null = null;
[1].forEach(() => {
  a = 1;
});

if (a !== null)
  a!.toFixed(); //

我不会使用null,而是使用undefined,因此不需要使用!

let a: number | undefined;
[1].forEach(() => {
  a = 1;
});

if (a) { // <-- if undefined or 0
  a.toFixed(); // No problem here
}

同样的建议是使用 !== 而非 !=

1
好的建议。使用undefined而不是null是一个很好的建议。 - Paleo
1
如果您将变量初始化为未定义,则它将不再起作用。因此,不仅 TypeScript 不知道(这很好),而且它确定它永远不会被分配(这是不正确的)。 let a: number | undefined = undefined; if(a) { // a 是 Never } - mtone
1
为什么要使用 undefined 来初始化变量? - distante

4
来晚了,但这是我的两分钱。

对被接受的答案的评论

if (a) {
  a.toFixed(); // No problem here
}

请注意,当 a 的值为 0 时,if 代码块不会被调用。

  • 要解决这个问题,请使用 if (a !== undefined)
  • 否则(当您真的不想处理 0 时),最好将 a 初始化为 0,例如:
    let a = 0; // typescript will infer the type number
    ...
    if (a) {
      // a is of type number and !== 0
    }

回复评论

为什么要使用 undefined 来初始化变量?

有些人这样做是因为一些工具(如IDE、linters等)否则会报告错误/警告。

例如,当您使用默认的TypeScript设置在IntelliJ IDEA中操作时,就会出现该警告:
enter image description here

我建议关闭这些检查,因为在 JavaScript 中未初始化的变量总是具有值 undefined。换句话说,在其他某些语言(如 C 语言)中,变量可能具有某些随机的“垃圾”值。

来自 MDN: Global_Objects/undefined#description 的引用

未被赋值的变量的类型为 undefined。

对于所有其他值(即不是undefined的值),TypeScript 编译器都会显示一个错误:
TS2454:变量 'xxx' 在赋值之前被使用。

回答原问题

let a: number | null = null;
[1].forEach(() => {
  a = 1;
});

if (a != null)
  a.toFixed(); // Error: Property 'toFixed' does not exist on type 'never'.

只有在编译器选项 strictNullChecks 开启时才会发生此问题。
这段引用描述了原因(引用来源):
“虽然 strictNullChecks 意味着仅检查变量的使用是否为 undefined 或 null,但实际上它将编译器置于非常悲观的模式下,当没有上下文推断类型的方式时,它将选择最狭窄的类型而不是最宽泛的类型。”
具体来说,这意味着:
- 由于 TypeScript 编译器不够聪明,无法知道 forEach 循环何时被调用(从而分配值),它采取悲观的方法,并假定 x 仍为 null - 因此,循环后 x 的类型为 null(不是我们可能期望的 number | null 类型) - 现在,最终的 if 块检查 x !=== null,这永远不可能发生(因为 TypeScript 假设 x 在执行 if 语句时 null)。 因此,在 if 语句内部,x 的类型为 never - 因此,“解决方案”之一是使用 x!.toFixed() 明确告诉 TypeScript,您确定 x 的值已定义。
其他:

Misc

strictNullChecks

当关闭 strictNullChecks 时,代码可正常工作: TypeScript 示例:strictNullChecks=off
我强烈不建议这样做。

for..of 循环

当你使用for..of循环而不是forEach()时,即使开启了strictNullChecks,代码也可以正常工作:Playground

let a: number | null = null;
for (const i of [1]) {
  a = 1;
};
if (a != null)
  a.toFixed();

其他初始值

您还可以考虑其他初始化值(而不是undefinednull):演示区

let a = 0; // typescript will infer that a is of type number
[1].forEach(() => {
  a = 1;
});
if (a >= 0)
  a.toFixed();


let b = NaN; // typescript will infer that b is of type number
[1].forEach(() => {
  a = 1;
});
if (!isNaN(b))
  b.toFixed();

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接