likes
comments
collection
share

TS类型体操(三) TS内置工具类2

作者站长头像
站长
· 阅读数 19

TS类型体操(三) TS内置工具类2

TS类型体操(一) 基础知识

TS类型体操(二) TS内置工具类1

这一篇继续研究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作为一个值的时候,它是一个类构造器,但是作为一个类型时,它却是自身的实例类型——不知道这句话有没有把你绕晕。

TS类型体操(三) TS内置工具类2

如上图所示,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本身。