Typescript 通过联合推断通用参数类型

xia*_*glu 1 typescript

我已经在下面的代码上苦苦挣扎了几个小时。不明白为什么e4不是?stringString

type PropConstructor4<T = any> = { new(...args: any[]): (T & object) } | { (): T }
type e4 = StringConstructor extends PropConstructor4<infer R> ? R : false // why string not String ???
Run Code Online (Sandbox Code Playgroud)

我在下面进行了测试,我想我可以理解。

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

另外,我不明白为什么e5不是?Stringstring

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

jca*_*alz 5

TL;DR 编译器使用启发式方法为不同的推理站点赋予不同的优先级,并从优先级最高的推理站点中推断出类型。


一般来说,当 TypeScript 推断类型参数的特定类型时(R在所有示例中),它会考虑每个推断站点,这是类型参数在它尝试匹配的表达式中出现的位置。例如,在

type P = StringConstructor extends 
    (() => infer R) | { new(...args: any[]): (infer R & object) } ? R : never
//         ^^^^^^^  <-- inference sites -->   ^^^^^^^
Run Code Online (Sandbox Code Playgroud)

类型参数有两个推断点R。编译器的工作是通过检查推理站点来尝试StringConstructor与完整表达式匹配,为该站点提出候选特定类型,然后检查完​​整表达式以确定该候选在每个站点中替换时是否有效。

让我们用上面的方法来测试它P,假装我们是编译器,看看如果我们更改检查的推理站点会发生什么。


如果编译器选择第一个推理站点进行检查:

type P = StringConstructor extends 
    (() => infer R) | { new(...args: any[]): (infer R & object) } ? R : never
//         ^^^^^^^  <-- inspect this
Run Code Online (Sandbox Code Playgroud)

在这种情况下,string是它会提出的候选者,因为String("hello")会产生string输出。然后它可以检查string整个表达式是否有效。 StringConstructor确实扩展了(() => string) | { new(...args: any[]): (string & object) },因为它扩展了联合的第一个成员(因为A extends B | CifA extends B或 为true A extends C),因此R将被推断为string编译器只考虑第一个推理站点。


那么第二个推理站点呢?

type P = StringConstructor extends 
    (() => infer R) | { new(...args: any[]): (infer R & object) } ? R : never
//                           inspect this --> ~~~~~~~~
Run Code Online (Sandbox Code Playgroud)

在这种情况下,String是它会提出的候选者,因为new String("hello")会产生String输出。然后它会检查String整个表达式是否有效。 StringConstructor确实延伸了,(() => String) | { new(...args: any[]): (String & object) }因为它延伸了联盟的双方。(stringextends String, so () => stringextends () => String),并且 soR会被推断,就String好像编译器只考虑第二个推断站点一样。


编译器也有可能同时考虑两个推理站点,并根据方差合成候选者的并集/超类型或交集/子类型。在这种情况下,参数都处于协变位置,因此string | String或只是String(因为它是 的超类型string)将是我的猜测,如果发生这种情况。


那么对于上面的表达式,我们可以合理地想象得出, string, Stringstring | String到底发生了什么?

type P = StringConstructor extends
    (() => infer R) | { new(...args: any[]): (infer R & object) } ? R : never
// string
Run Code Online (Sandbox Code Playgroud)

它是string。这意味着编译器优先考虑第一个推理站点。与以下情况进行比较:

type O = StringConstructor extends
    (() => infer R) | { new(...args: any[]): (infer R) } ? R : never
// String
Run Code Online (Sandbox Code Playgroud)

现在,编译器优先考虑第二个推理站点。不知何故,(infer R & object)的优先级比 低infer R


那么,编译器如何为不同的推理站点分配优先级呢?我不能假装知道这件事的全部细节。

曾经在 TypeScript 规范文档中列出,但早已过时,现已存档。如果您好奇,请参阅本节。现在没有规范,因为语言的变化速度快于严格记录的速度。

GitHub 上有几个问题涉及推理站点优先级的想法。请参阅microsoft/TypeScript#14829了解允许站点具有零优先级并且永远不会用于推理的功能请求。请参阅microsoft/TypeScript#39295microsoft/TypeScript#32389,了解由于开发人员的直觉与编译器实际执行的操作不同而产生的问题。

一个常见的线索是,类似的交叉点的(T & {})优先级低于没有交叉点的类型TT & {}因此,如果某个站点妨碍了您的工作,您可以降低该站点的优先级。所以你解释了为什么infer R & object没有选择网站P

再说一次,我不知道这方面的全部细节,而且学习起来可能不太有启发性。如果您仔细检查类型检查器代码,您可能能够拼凑出构造签名返回类型是否比调用签名返回类型具有更高的优先级,但我不建议编写任何依赖于此类细节的程序,除非您想继续重新访问它...人们无法保证这种特定的推理规则将在该语言的各个版本中持续存在。


Playground 代码链接