这是代码( 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' implicitly has type 'any' because it does not have a type annotation and is referenced directly or indirectly in its own initializer.
据我所知,在循环的每次迭代开始时,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 确实假定索引将出现在记录中。 最佳答案
见 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
分配取决于任何先前的此类缩小,例如分配 current = pointers[key]
这可能发生在前一个循环的末尾。 pointers[key]
的类型取决于 pointers
的类型和 key
的类型. pointers
的类型注释为 Record<string, XY>
并且没有通过控制流分析缩小范围,所以我们可以停止看这里。 key
的类型取决于...嘿等一下,检测到圆形! 🤯 无论如何,这都不是理想的编译器行为。但这并不是 TypeScript 中的真正错误,因为
key
的初始化程序引用 current
第二次通过循环 current
有一个引用 key
的赋值.所以key
确实在其初始值设定项中间接引用了自己……这是非常可靠的“设计限制”领域。当然,在上面的许多要点中,一个理性的人可能在行为上与编译器不同。例如,考虑
current.x + ','
的类型取决于 current.x
的类型和 ','
的类型一般而言,形式为
a + b
的表达式的类型是正确的。取决于 a
的类型和b
的类型, a
有一些特殊类型(或 b
)这意味着您可以“短路”类型分析并完全忽略 b
的类型(或 a
)。在上述情况下,由于 current.x + ','
正在添加 string
至 current.x
,结果肯定是string
不管怎样current.x
原来是。不幸的是,编译器在这里没有做这样的分析。也许有人可以在 GitHub 上提出这样的问题,但我不知道它会被实现。总的来说,这种对“可短路”表达式的额外检查可能会在编译器性能方面收回成本。但如果它降低了编译器的平均性能,那么治愈可能比疾病更糟糕。看到这样的功能请求会很有趣,我肯定会给它一个 👍,但我对它被采用不会很乐观。
无论如何,您在问题中谈论的更改会破坏上述链的某些部分,并防止编译器掉入循环漏洞。显式注释
key
如 string
是明显的修复,并使编译器现在只检查 key
的类型而不是推断它。当它再次到达key
在 current = pointers[key]
,它知道key
是 string
并且可以继续前进。
关于typescript - 为什么 typescript 说这个变量是 "referenced directly or indirectly in its own initializer"?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/67660342/