【发布时间】:2021-08-12 01:35:52
【问题描述】:
这是代码 (Playground Link):
interface XY {x: number, y: number}
function mcve(current: XY | undefined, pointers: Record<string, XY>): void {
if(!current) { throw new Error(); }
while(true) {
let key = current.x + ',' + current.y;
current = pointers[key];
}
}
这个例子中的代码并不是为了做任何有用的事情;我删除了证明问题所不需要的所有内容。 Typescript 在编译时在声明变量 key 的行报告以下错误:
'key' 隐式具有类型'any',因为它没有类型注释,并且在其自己的初始化程序中直接或间接引用。
据我所知,在循环的每次迭代开始时,Typescript 都知道current 被缩小为XY 类型,并且current.x 和current.y 都属于number 类型,因此可以直接确定表达式current.x + ',' + current.y 的类型为string,并推断key 的类型为string。在我看来,字符串连接应该显然是string 类型。但是,Typescript 不这样做。
我的问题是,为什么 Typescript 不能推断出 key 的类型是 string?
在调查该问题时,我发现了一些导致错误消息消失的代码更改,但我无法理解为什么这些更改对 Typescript 很重要代码:
- 给
key一个显式类型注释: string,这是我在真实代码中所做的,但这并不能帮助我理解为什么不能推断。 - 注释掉
current = pointers[key]行。在这种情况下,key被正确推断为string,我不明白为什么随后分配给current会使这更难推断。 - 将
current参数的类型从XY | undefined更改为XY;我不明白为什么这很重要,因为current在循环开始时确实有类型XY通过控制流类型缩小。 (如果不是,那么我预计像 "current这样的错误可能是undefined" 而不是实际的错误消息。) - 将
current.x和current.y替换为number类型的一些其他表达式。我不明白为什么这很重要,因为current.x和current.y在该表达式中确实有number类型。 - 将
pointers替换为(s: string) => XY类型的函数,并将索引访问替换为函数调用。我不明白为什么这很重要,因为Record<string, XY>的索引访问似乎应该等同于(s: string) => XY类型的函数调用,因为 Typescript 确实假设索引将出现在记录中。
【问题讨论】:
-
现在不在真正的计算机上,但我怀疑这是类型推断算法的限制;依赖标记的发生顺序可能使这成为一种病态的情况:
key的类型取决于current;current的类型会因为 CFA 变窄而改变,所以它的类型取决于pointers[key]的类型,这是一个明显的循环。编译器无法通过以不同顺序评估类型来“看到”循环性是可移除的,这令人遗憾,但可能不应该太令人惊讶。 -
感谢您的评论@jcalz - 我看不出
key的类型如何取决于current的类型;无论current是什么,字符串连接要么产生一个字符串,要么在key收到值之前由current.x抛出异常(如果current是undefined)。 -
我相信你应该创建一个问题。行为有点奇怪。如果您要创建问题,请不要忘记添加我将订阅的链接
-
@kaya3 再次,您正在查看
current.x + ','之类的表达式并进行??? + string → string形式的“短路”类型分析,其中???可以完全忽略。但是编译器不会做那种短路。它不会意识到这一点,直到它将两个操作数的类型评估为+,然后解析+的各种“重载”中的哪一个适用于这两种类型。所以它在到达下一步之前就掉进了圆形的洞里。