几个小时以来,我一直在为下面的代码苦苦挣扎。不明白为什么 e4
不是 string
不是 String
?
type PropConstructor4<T = any> = { new(...args: any[]): (T & object) } | { (): T }
type e4 = StringConstructor extends PropConstructor4<infer R> ? R : false // why string not String ???
我在下面测试过,我想我能理解。
type a4 = StringConstructor extends { new(...args: any[]): (infer R & object) } ? R : false // String
type b4 = StringConstructor extends { (): ( String) } ? true : false // true
type c4 = StringConstructor extends { (): (infer R) } ? R : false // string
另外,我不明白为什么 e5
是 String
不是 string
?
type PropConstructor5<T = any> = { new(...args: any[]): (T ) } | { (): T }
type e5 = StringConstructor extends PropConstructor5<infer R> ? R : false //why String not string??
答案 0 :(得分:4)
TL;DR 编译器使用启发式方法为不同的推理站点赋予不同的优先级,并从具有最高优先级的推理站点推断类型。
一般来说,当 TypeScript 为类型参数(所有示例中的 R
)推断特定类型时,它会考虑每个推断站点,其中是类型参数出现在它试图匹配的表达式中的位置。例如,在
type P = StringConstructor extends
(() => infer R) | { new(...args: any[]): (infer R & object) } ? R : never
// ^^^^^^^ <-- inference sites --> ^^^^^^^
R
类型参数有两个推理站点。编译器的工作是通过检查推理站点来尝试将 StringConstructor
与完整表达式匹配,为该站点提出候选特定类型,然后检查 确定该候选人在每个站点中替换时是否有效的完整表达式。
让我们用上面的 P
来测试一下,假设我们是编译器,看看如果我们更改要检查的哪个推理站点会发生什么。
如果编译器选择第一个推理站点进行检查:
type P = StringConstructor extends
(() => infer R) | { new(...args: any[]): (infer R & object) } ? R : never
// ^^^^^^^ <-- inspect this
在这种情况下,string
是它会提出的候选者,因为 String("hello")
产生一个 string
输出。然后它可以检查 string
是否适用于整个表达式。 StringConstructor
确实扩展了 (() => string) | { new(...args: any[]): (string & object) }
,因为它扩展了联合的第一个成员(因为 A extends B | C
在 A extends B
或 A extends C
时为真),所以 {{1如果编译器只考虑第一个推理点,则}} 将被推断为 R
。
第二个推理站点怎么样?
string
在这种情况下,type P = StringConstructor extends
(() => infer R) | { new(...args: any[]): (infer R & object) } ? R : never
// inspect this --> ~~~~~~~~
是它会提出的候选者,因为 String
产生一个 new String("hello")
输出。然后它会检查 String
是否适用于整个表达式。 String
确实扩展了 StringConstructor
,因为它扩展了联合的两边。 ((() => String) | { new(...args: any[]): (String & object) }
extends string
,所以 String
extends () => string
),所以如果编译器只考虑第二个,() => String
将被推断为 R
推理站点。
编译器也有可能同时考虑两个推理站点,并根据方差合成候选者的联合/超类型或交叉/子类型。在这种情况下,参数都处于协变位置,因此如果发生这种情况,我会猜测 String
或只是 string | String
(因为它是 String
的超类型)。
那么对于上面的表达式,我们可以合理地想象 string
, string
, String
出来了。实际发生了什么?
string | String
是 type P = StringConstructor extends
(() => infer R) | { new(...args: any[]): (infer R & object) } ? R : never
// string
。这意味着编译器给第一个推理站点优先。与以下情况进行比较:
string
现在,编译器优先考虑第二个推理站点。不知何故,type O = StringConstructor extends
(() => infer R) | { new(...args: any[]): (infer R) } ? R : never
// String
的优先级低于 (infer R & object)
。
那么,编译器如何为不同的推理站点分配优先级?我无法假装知道这件事的全部细节。
它使用在 TypeScript 规范文档中进行了布局,但是已经过时并且现在已存档。如果您好奇,请参阅 this section。现在没有规范,因为语言变化的速度比严格记录的要快。
GitHub 中有一些问题与推理站点优先级的想法有关。请参阅 microsoft/TypeScript#14829 以获取允许站点具有零优先级并且永远不会用于推理的功能请求。请参阅 microsoft/TypeScript#39295 和 microsoft/TypeScript#32389,了解因开发人员的直觉与编译器的实际操作不同而引起的问题。
一个常见的思路是像 infer R
这样的交集比没有交集的类型具有更低的优先级 (T & {})
。因此,您可以使用 T
来降低网站的优先级,如果它妨碍了。所以你解释了为什么 T & {}
不是 infer R & object
选择的网站。
再说一次,我不知道这件事的全部细节,学习起来可能不是很有启发性。如果您通过 the type checker code,您也许可以拼凑出 construct 签名返回类型是否比 call 签名返回类型具有更高的优先级,但我不建议编写任何依赖于这些细节的程序,除非你想不断地重新审视它……不能保证这种特定的推理规则会在语言的各个版本中持续存在。