打字稿推断通用参数类型与联合

时间:2021-02-27 13:59:58

标签: typescript

几个小时以来,我一直在为下面的代码苦苦挣扎。不明白为什么 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

另外,我不明白为什么 e5String 不是 string

type PropConstructor5<T = any> = { new(...args: any[]): (T ) } | { (): T }
type e5 = StringConstructor extends PropConstructor5<infer R> ? R : false //why String not string??

1 个答案:

答案 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 | CA extends BA 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#39295microsoft/TypeScript#32389,了解因开发人员的直觉与编译器的实际操作不同而引起的问题。

一个常见的思路是像 infer R 这样的交集比没有交集的类型具有更低的优先级 (T & {})。因此,您可以使用 T 来降低网站的优先级,如果它妨碍了。所以你解释了为什么 T & {} 不是 infer R & object 选择的网站。

再说一次,我不知道这件事的全部细节,学习起来可能不是很有启发性。如果您通过 the type checker code,您也许可以拼凑出 construct 签名返回类型是否比 call 签名返回类型具有更高的优先级,但我不建议编写任何依赖于这些细节的程序,除非你想不断地重新审视它……不能保证这种特定的推理规则会在语言的各个版本中持续存在。


Playground link to code