在使用map时理解对象中未定义的键



这是MWE:

interface Animals {cat:string, dog:string}
let a: Animals|undefined;

if(a){// if we got `a` from somewhere
[1,2,3].map(v=>a.cat)
// ----------> ~ Object is possibly 'undefined'
}

游乐场

我不确定错误到底是什么,它似乎与映射输出的东西或未定义有关,但为什么a没有定义?

这是TypeScript的一般限制,在microsoft/TypeScript#9998中有描述。控制流分析的效果,例如在if (a)的真实性检查之后,aAnimals | undefined缩小到Animals,并不会持续到跨功能边界的封闭值。

在回调函数v => a.cat的内部,因此,a的控制流没有变窄;它被认为是Animals | undefined类型,因此在索引它时会得到一个错误。

发生这种情况的原因是因为编译器目前没有办法知道回调v => a.cat立即运行。这是我们关于Array.prototype.map()的带外信息,但是从类型系统的角度来看,[1,2,3].map和以下foo的定义之间没有意义的区别:

function foo(cb: (x: number) => string) {
setTimeout(() => cb(100), 1000);
}
a = { cat: "abc", dog: "def" };
if (a) {
foo(v => a.cat) // error!
// ----> ~ Object is possibly 'undefined'
}
a = undefined;
// later: Uncaught TypeError: a is undefined

foo()函数接受一个回调,并在之后调用它。实际上,当它调用它时,aundefined。所以编译器抱怨通常是正确的。你可以希望编译器可能会注意到a是否被重新赋值,如果没有,则会抑制错误,但这对编译器来说是大量额外的工作,而microsoft/TypeScript#9998的重点是他们还没有找到任何可以在编译器性能方面为自己付出代价的东西。

在microsoft/TypeScript#11498中有一个特性请求,允许类型系统被告知map()立即运行它的回调,这样任何控制流分析结果都可以在回调中持久化,这将允许map()foo()被区别对待…但是现在它还不是语言的一部分。


这就是问题所在。编译器不知道回调会立即运行,它不会搜索源代码来检查a是否被重新分配给undefined,所以它在安全方面出错。

当前最好的解决方法是将缩小的值分配给一个新的const变量,该变量的类型根据定义已经缩小:

if (a) {
const _a = a;
[1, 2, 3].map(v => _a.cat) // okay
}

存在的_a变量总是Animals类型,而不是Animals | undefined类型,因此回调类型检查成功。

Playground链接到代码

这是因为TypeScript处理变量重赋的不确定性。下面是关于设计选择的讨论。如果您将声明交换为const,则错误将在流部分中清除并在初始化时标记。

最新更新