【问题标题】:Why does Typescript say this variable is "referenced directly or indirectly in its own initializer"?为什么 Typescript 说这个变量是“在它自己的初始化程序中直接或间接引用的”?
【发布时间】: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.xcurrent.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.xcurrent.y 替换为number 类型的一些其他表达式。我不明白为什么这很重要,因为 current.xcurrent.y 在该表达式中确实有 number 类型。
  • pointers 替换为(s: string) =&gt; XY 类型的函数,并将索引访问替换为函数调用。我不明白为什么这很重要,因为 Record&lt;string, XY&gt; 的索引访问似乎应该等同于 (s: string) =&gt; XY 类型的函数调用,因为 Typescript 确实假设索引将出现在记录中。

【问题讨论】:

  • 现在不在真正的计算机上,但我怀疑这是类型推断算法的限制;依赖标记的发生顺序可能使这成为一种病态的情况:key 的类型取决于current current 的类型会因为 CFA 变窄而改变,所以它的类型取决于 pointers[key] 的类型,这是一个明显的循环。编译器无法通过以不同顺序评估类型来“看到”循环性是可移除的,这令人遗憾,但可能不应该太令人惊讶。
  • 感谢您的评论@jcalz - 我看不出key 的类型如何取决于current 的类型;无论current 是什么,字符串连接要么产生一个字符串,要么在key 收到值之前由current.x 抛出异常(如果currentundefined)。
  • 我相信你应该创建一个问题。行为有点奇怪。如果您要创建问题,请不要忘记添加我将订阅的链接
  • @kaya3 再次,您正在查看current.x + ',' 之类的表达式并进行??? + string → string 形式的“短路”类型分析,其中??? 可以完全忽略。但是编译器不会做那种短路。它不会意识到这一点,直到它将两个操作数的类型评估为+,然后解析+ 的各种“重载”中的哪一个适用于这两种类型。所以它在到达下一步之前就掉进了圆形的洞里。

标签: typescript type-inference


【解决方案1】:

请参阅 microsoft/TypeScript#43047 了解此类问题的规范答案。

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

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

  • key的类型取决于current.x + ',' + current.y的类型,这取决于current.x + ','的类型
  • current.x + ',' 的类型取决于current.x 的类型和',' 的类型
  • current.x 的类型取决于current 的类型。
  • 由于current 是一个联合类型变量,它的表观类型可以是narrowed via control flow analysis,因此它在分配key 时的类型取决于任何先前的此类缩小,例如分配@987654337 @ 这可能发生在上一个循环的末尾。
  • pointers[key] 的类型取决于pointers 的类型和key 的类型。
  • pointers 的类型被注解为Record&lt;string, XY&gt; 并且没有通过控制流分析来缩小范围,所以我们可以不看这里了。
  • key 的类型取决于...嘿等一下,检测到循环! ?

无论如何,编译器的行为都是不可取的。但这并不是 TypeScript 中的真正错误,因为key 的初始化程序引用了current,并且第二次循环current 有一个引用key 的赋值。所以key 确实在其初始化程序中间接引用了自己……这完全是“设计限制”领域。


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

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

虽然一般来说a + b 形式的表达式的类型确实取决于a 的类型和b 的类型,但a(或@987654356 @) 这意味着您可以“短路”类型分析并完全忽略b(或a)的类型。在上述情况下,由于current.x + ',' 是在current.x 中添加string,所以无论current.x 是什么,结果肯定是string

很遗憾编译器在这里没有做这样的分析。也许有人可以在 GitHub 上提出这样的问题,但我不知道它会被实现。总的来说,这种对“可短路”表达式的额外检查可能会在编译器性能方面为自己付出代价。但如果它降低了编译器的平均性能,那么治疗可能比疾病更糟糕。看到这样的功能请求会很有趣,我肯定会给它一个?,但我不会很乐观地接受它。


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

【讨论】:

  • 感谢您的详细回答。我最初将其命名为let key = `${current.x},${current.y}`;,这完全有同样的问题;我很惊讶编译器认为字符串插值的类型可能不是string,但也许它的分析方式与连接相同,然后“可能这个+意味着数字加法”的歧义是虚假介绍?在某些情况下,或者编译器认为字符串插值的类型可能比string 更严格。
  • 我猜对于 + 和模板文字,编译器都将它们视为表达式,取决于组成表达式,而不是向前看是否可以在不评估输入类型的情况下确定输出类型.对于模板文字,我原以为它只会假设string 输出而不关心输入,但我想不会。也许他们只是碰巧没有实现它,或者它可能是一个性能问题,或者这样做可能更棘手,因为通用模板文字表达式包括可以具有任何输出类型的标记模板。我很好奇。
猜你喜欢
  • 2020-10-03
  • 2019-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-04-13
  • 2018-08-20
  • 2014-11-17
  • 2021-07-11
  • 1970-01-01
相关资源
最近更新 更多