TS类型体操(三) TS内置工具类2
TS类型体操(三) TS内置工具类2
这一篇继续研究TS的内置工具类:Parameters
ConstructorParameters
ReturnType
InstanceType
等等。
这些工具类将用到infer
类型推导,我们需要先研究一下infer
的使用。
如果你对文章内容有疑问,或是发现有错误,欢迎批评指正!
infer类型推导
我们先看一个最简单的例子:
type I<T> = T extends infer R ? R : never
type i = I<0 | 1> // 0 | 1
我们可以将infer R
视作是一个 占位类型 ,TS会根据它所占的位置,推导出它的类型,上例中的R
推导的结果就是T
自身。
我们知道,extends
用于判断类型的父子集关系,如果要对T extends infer R
中的R
进行类型推导,前提是它必须是T
的父集,假如是这种情况:
type I<T> = T extends [infer R] ? R : 'MISS MATCH'
type i1 = I<string> // 'MISS MATCH' - extends为假 不可能推导出R
type i2 = I<[string]> // string - extends为真 推导出R为string
这里infer R
占位于一个数组内,如果T
不是数组,extends
会判断为假,推导R
的类型根本没有意义,而且也不可能推导出结果。
因此,infer只能用在extends后面,推导的类型则只能用在extends判断为真的位置。
type I<T> = T extends <infer只能在这个范围内使用> ? <R只能在这个范围内使用> : <R不能用在这里>
再看个元组的例子:
type Head<T> = T extends [infer R, ...any] ? R : never
type h = Head<[string, number, boolean]> // string
这样可以获取元组的第一项,这种做法在类型体操中是很常用的。另外,这个例子还告诉我们,TS也是支持用...
来表示剩余项的。
infer用于对象时,既可以占位键的类型,也可以占位值的类型。
type V<T> = T extends { a: infer A } ? A : never // 推导值的类型
type i = V<{ a: 1; b: 2; c: 3 }> // 1
type K<T> = T extends { [k in infer R]: any } ? R : never // 推导键的类型
type k = K<{ a: 1; b: 2; c: 3 }> // "a" | "b" | "c"
infer用于函数时,既可以占位参数的类型,也可以占位返回值的类型。
type Arg<T> = T extends (a: infer A) => any ? A : never // 推导函数的参数类型
type Return<T> = T extends () => infer R ? R : never // 推导函数的返回值类型
同infer多占位问题
上面例子的infer
都只有一个占位,如果同一个infer
有多个占位会怎么样呢?这需要分情况来讨论:
- 一般情况下,TS会按 联合类型 处理
type I<T> = T extends [infer A, infer A] ? A : 'MISS MATCH'
type i1 = I<[string, number]> // string | number
type i2 = I<[1, 2]> // 1 | 2
这里infer A
占了两个位置,如果两个位置类型不同,TS推导A是两个类型的联合类型。
- 如果
infer
占位于函数的参数,TS会按 交叉类型 处理
type F<T> = T extends (a: infer A, b: infer A) => any ? A : 'MISS MATCH'
type f1 = F<(a: string, b: number) => void> // never - string & number 结果当然是never
type f2 = F<(a: 0, b: number) => void> // 0 - 0 & nunber,结果自然是0
如果参数是元组,infer
占位于元组中,结果也是交叉类型:
type I<T> = T extends (a: [infer A, infer A]) => any ? A : 'MISS MATCH'
type i = I<(a: [string, number]) => any> // never
-
如果
infer
有的位于函数参数,有的位于其他地方,比如函数的返回值。首先,TS会分别推导参数位置的类型(交叉类型)和返回值的类型(联合类型),然后再对比:
-
如果参数与返回值类型相同,直接取它为结果。
-
如果参数与返回值的类型不同,且没有父子集关系,extends会判断为假。
-
最特殊的是这一条:如果返回值类型是参数类型的子集,推导结果为返回值类型。
-
先看一些两个infer
的例子:
type F<T> = T extends (a: infer A) => infer A ? A : 'MISS MATCH'
type f1 = F<(a: string) => string> // string - 类型相同
type f2 = F<(a: string) => number> // 'MISS MATCH' - 类型不同,extends判断为假
type f3 = F<(a: string | number) => number> // number - 返回值类型是参数类型的子集,结果为返回值类型
type f4 = F<(a: string) => string | number> // 'MISS MATCH' - 返回值类型是参数类型的父集,extends判断为假
一些多个infer
的例子:
type F<T> = T extends (a: infer A) => [infer A, infer A] ? A : 'MISS MATCH'
type f1 = F<(a: string | number) => [0, string]> // string | 0
type f2 = F<(a: number) => [0, number]> // number
type f3 = F<(a: string) => [number, string]> // 'MISS MATCH'
type f4 = F<(a: number) => [0, 2]> // 0 | 2
ReturnType
ReturnType
用于获取函数的返回值类型
type ReturnType<T> = T extends (...args: any) => infer R ? R : any
很简单,没什么好说的。
Parameters
Parameters
用于获取函数的参数类型
type Parameters<T> = T extends (...args: infer P) => any ? P : never
也很简单,没什么好说的。
InstanceType
InstanceType
可以获取 类的实例类型 ,这个非常值得深入研究一下,也将是这一节的重点。
题外话:如果你是vue的使用者,可能经常会用到这个InstanceType,如果你没有用过它,请务必搜索一下它在vue中的应用场景。
InstanceType的用法
首先看看它的用法:
class Person {
name: string
age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
}
// InstanceType获取Person的实例类型
type PersonInstanceType = InstanceType<typeof Person>
// 创建Person的实例person,它的类型就是PersonInstanceType
const person: PersonInstanceType = new Person('John', 30)
我在本系列第一篇文章的开头就提到过,值
和类型
的区别,并强调二者不能混同。
但是,class声明的类类型(说实话我不知道该如何称呼它)是个例外,因为它 既是一个值,也是一个类型 ,如果让我发明一个词来形容它,那就是——值类二象性
……开个玩笑。
上面例子中的PersonInstanceType
就是Person
构造器的实例类型,但是,如果我们测试它和Person
的关系:
type t1 = PersonInstanceType extends Person ? true : false // true
type t2 = Person extends PersonInstanceType ? true : false // true
会发现二者作为类型是相等的——PersonInstanceType
相当于是Person
的别名,区别只在于,PersonInstanceType
是纯类型,而Person
具有“值类二象性”。
另外,我们注意到,InstanceType<typeof Person>
使用了typeof
,那这个typeof Person
和作为类型的Person
有什么区别呢?
首先可以肯定,二者并不相等,甚至不存在父子集关系,我们用同样的方式测试:
type t3 = typeof Person extends Person ? true : false // false
type t4 = Person extends typeof Person ? true : false // false
所以,如果去掉typeof
,会因为不符合泛型约束而报错:
type PersonInstanceType = InstanceType<Person> // 报错ts(2344)
报错信息:类型“Person”提供的内容与签名“new (...args: any): any”不匹配。ts(2344)
说到这里,不知道你有没有感觉到,似乎有什么地方很奇怪?
停顿,思考一下……
类的“值类二象性”
第一个奇怪的地方是,我们已经知道,InstanceType
的作用是获取类的实例类型,但我们测试发现,实例类型PersonInstanceType
实际上就是Person
自身作为类型的别名,也就是说:Person
作为一个值的时候,它是一个类构造器,但是作为一个类型时,它却是自身的实例类型——不知道这句话有没有把你绕晕。
如上图所示,typeof Person
目的就是为了获取Person
的构造器类型,构造器类型不等于实例类型,它们也不存在父子集关系,InstanceType
的作用就是根据构造器类型来获取它的实例类型。
构造器类型
第二个奇怪的地方的是,既然PersonInstanceType
就是Person
作为类型的自己,那我们干嘛要绕一大圈?
你可能早就发现了,上面的例子中,我们完全可以省略中间的InstanceType
:
class Person {
name: string
age: number
constructor(name: string, age: number) {
this.name = name
this.age = age
}
}
const person: Person = new Person('John', 30)
InstanceType
的意义何在?
首先第一个作用,它可以将Person
作为类型的部分提取出来。很多情况下,我们可能不希望作为构造器的Person
本身暴露出去,但又希望它的实例类型能够被外部使用,这时我们就可以用InstanceType
,只暴露类型,不暴露值。
class Person {}
export type PersonInstanceType = InstanceType<typeof Person>
然后第二个作用,这个解释起来有点麻烦,一言以蔽之:因为TS不仅支持class声明类, 同时也支持单独声明 构造器类型,InstanceType
可以用来获取单独声明的构造器类型的实例。
我们知道,所谓类构造器指的就是constructor
函数,那我们该如何单独声明一个构造器类型呢?
-
包含一个或多个 构造签名 的对象类型,就是构造器类型。
-
构造器类型可以使用 构造函数类型字面量 或 包含构造签名的对象类型字面量 来书写。
-
构造函数类型字面量
type MyConstructor = new (...args: any) => any
-
包含构造签名的对象类型字面量
type MyConstructor = { new (...args: any): any }
两种写法是等价的,我们可以使用
InstanceType
来获取该构造器类型的实例类型:type MyInstance = InstanceType<MyConstructor>
-
关于构造器类型的使用场景,这里有个示例:AbstractConstructor。
InstanceType的实现
type InstanceType<T> = T extends abstract new (...args: any) => infer R ? R : any
与它的作用相比,它的实现倒是平平无奇,唯一值得注意的是,这里添加了一个abstract
,表示抽象构造器,非抽象的构造器是抽象构造器的子集。
ConstructorParameters
ConstructorParameters
用于获取构造器类型的参数类型
type ConstructorParameters<T> = T extends abstract new (...args: infer P) => any ? P : never
ThisParameterType
ThisParameterType
用于获取函数中this的类型
type ThisParameterType<T> = T extends (this: infer U, ...args: never) => any ? U : unknown
ThisParameterType
只能用于显式指定了this
的函数,如果函数类型没有指定this
,结果是unknown
。
OmitThisParameter
OmitThisParameter
的作用和 ThisParameterType
相反,它可以清除函数的this。
type OmitThisParameter<T>
= unknown extends ThisParameterType<T>
? T
: T extends (...args: infer A) => infer R
? (...args: A) => R
: T
这里借助了ThisParameterType
判断T的this
类型,如果ThisParameterType<T>
结果是unknown
,说明T没有显式的指定this,结果就是T本身。
转载自:https://juejin.cn/post/7229895584038764603