为什么 Typescript 说这个变量“在它自己的初始化器中直接或间接引用”?

kay*_*ya3 5 type-inference typescript

这是代码(游乐场链接):

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];
    }
}
Run Code Online (Sandbox Code Playgroud)

此示例中的代码并不意味着做任何有用的事情;我删除了证明该问题不需要的所有内容。Typescript 在编译时在声明变量的行报告以下错误key

“key”隐式具有“any”类型,因为它没有类型注释,并且在其自己的初始值设定项中直接或间接引用。

据我所知,在循环的每次迭代开始时,Typescript 知道 已current缩小为 type XY,并且current.xcurrent.y均为 type number,因此应该很容易确定表达式current.x + ',' + current.y的类型为string,并推断出key属于 类型string。在我看来,字符串连接显然应该是 类型string。然而,Typescript 不这样做。

我的问题是,为什么 Typescript 无法推断出它的key类型string


在调查这个问题的过程中,我发现了对代码的一些更改,这些更改导致错误消息消失,但我无法理解为什么这些更改对这段代码中的 Typescript 很重要:

  • 给出key显式类型注释: string,这是我在真实代码中所做的,但这并不能帮助我理解为什么不能推断出这一点。
  • 注释掉该行current = pointers[key]。在这种情况下key,正确推断为string,并且我不明白为什么随后的分配current应该使这更难以推断。
  • current参数类型从更改XY | undefinedXY; 我不明白为什么这很重要,因为通过控制流类型缩小在循环开始时current确实有类型。XY(如果没有,那么我预计会出现current可能是undefined这样的错误,而不是实际的错误消息。)
  • current.xand替换current.y为其他一些类型的表达式number。我不明白为什么这很重要,因为current.xandcurrent.y确实有number该表达式的类型。
  • pointers用类型函数替换(s: string) => XY并用函数调用替换索引访问。我不明白为什么这很重要,因为 a 的索引访问Record<string, XY>似乎应该相当于 type 的函数调用(s: string) => XY,因为 Typescript 确实假设索引将出现在记录中。

jca*_*alz 4

有关此类问题的规范答案,请参阅microsoft/TypeScript#43047 。

这是 TypeScript 类型推断算法的设计限制。一般来说,为了让编译器推断给定x初始化赋值的变量的类型x,它需要知道被赋值的表达式的类型。如果该表达式包含对其他未显式注释其类型的变量的引用,则它也需要推断这些变量的类型。如果此依赖链在解析之前返回x,编译器就会放弃并声明x在其自己的初始值设定项中引用它。

在你的情况下,我想编译器的分析是这样的(我不是编译器专家,所以这只是说明性的,而不是规范的):

  • 的类型key取决于 的类型current.x + ',' + current.y,而 的类型又取决于 的类型current.x + ','
  • 的类型current.x + ','取决于 的类型current.x和 的类型','
  • 的类型current.x取决于 的类型current
  • 由于current是联合类型变量,因此可以通过控制流分析来缩小其表观类型,因此其在赋值点的类型key取决于任何先前的此类缩小,例如current = pointers[key]可能发生在 a 末尾的赋值上一个循环。
  • 的类型pointers[key]取决于 的类型pointers和 的类型key
  • 的类型pointers被注释为是Record<string, XY>,并没有通过控制流分析来缩小范围,所以我们可以不再看这里了。
  • 类型key取决于...嘿等一下,已检测到圆度!

无论如何,这都不是理想的编译器行为。但这实际上并不是TypeScript 中的错误,因为key的初始值设定项引用current,并且第二次循环时current有一个引用的赋值key。因此key,确实在其初始化程序中间接引用了自身……这确实是“设计限制”领域。


当然,在上述许多要点上,一个理性的人的行为可能与编译器有很大不同。例如,考虑

  • 的类型current.x + ','取决于 的类型current.x和 的类型','

虽然一般来说,形式表达式的类型确实a + b取决于 的类型a和 的类型,但(或)b有一些特定类型,这意味着您可以“短路”类型分析并完全忽略(或)的类型。在上面的例子中,由于添加了 a ,所以无论结果如何,结果都肯定是 a 。abbacurrent.x + ','stringcurrent.xstringcurrent.x

不幸的是编译器在这里不做这样的分析。也许有人可以在 GitHub 中提出一个问题来要求这样的,但我不知道它会被实现。总的来说,这种对“可短路”表达式的额外检查可能会在编译器性能方面得到回报。但如果它降低了编译器的平均性能,那么治疗方法可能比疾病更糟糕。看到这样的功能请求会很有趣,我肯定会给它一个 ,但我对它被采用并不是很乐观。


无论如何,您在问题中谈论的更改会破坏上述链的某些部分,并防止编译器陷入循环漏洞。显式注释keyasstring是明显的修复方法,并且使编译器现在只需检查类型key而不是推断类型。当它再次到达keyin时current = pointers[key],它知道key是 astring并且可以继续前进。