likes
comments
collection
share

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

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

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

上一篇是基础知识,虽然是基础知识,但非常重要,没看的建议先看上一篇。

从这一篇开始,我们来研究一下TS内置的工具类,看看类型体操具体是怎么玩的。

如果对本篇内容有疑问,或发现有错误,欢迎批评指正!!

Exclude<T, U>——求补集

TS内置工具类Exclude<T, U>可以将联合类型的一部分排除,从集合的角度来看,相当于求补集:

type A = string | undefined
type B = Exclude<A, undefined>
//--------------------------------------------
// 结果:
type B = string

Exclude实现非常简单:

type Exclude<T, U> = T extends U ? never : T

还记得我在上一篇中讲到的联合类型分发(distributive conditional types)吗?

这里其实就用到了这个机制,以上面的例子来说,它的计算过程是这样的:

type A = string | undefined
type B = Exclude<A, undefined>
// 相当于↓
type B = string extends undefined ? never : string | undefined extends undefined ? never : undefined
// 结果↓
type B = string | never
// 结果↓
type B = string

NonNullable<T>——排除空值

TS内置工具类NonNullable<T>可以将类型中的空值(nullundefined)排除,例如:

type A = string | undefined | null
type B = NonNullable<A>
//--------------------------------------------
// 结果
type B = string

看到这里,聪明的你也许会觉得,它的实现应该是借助了Exclude:

type MyNonNullable<T> = Exclude<T, undefined | null>

是的,从效果来看,这么做和NonNullable是一模一样的,但事实上,TS却用了个更简单的做法:

type NonNullable<T> = T & {}

乍一看你也许会满脑子问号——这啥?

这里的{},其实它并不是空对象,而是表示一个特殊的非空类型。

特殊的非空类型:{}

上一篇文章我提到过,从集合的角度来看,unknown相当于全集,其他所有类型都是unknown的子集,而{}则是范围仅次于unknown的特殊类型,{}加上null、再加上undefined,就等于unknown

我们可以这样验证:

type T = unknown extends {} | undefined | null ? true : false // true

明白了这个,我们就知道为什么T & {}能排除空值了——因为 {}是除空值以外其他所有类型的父集

type t1 = string extends {} ? true : false // ture
type t2 = number extends {} ? true : false // ture
type t3 = [] extends {} ? true : false // ture
type t4 = true extends {} ? true : false // ture
type t5 = object extends {} ? true : false // ture
type t6 = undefined extends {} ? true : false // false
type t7 = null extends {} ? true : false // false

映射类型

TS内置工具类 Record Partial Required Readonly Pick Omit ,都是通过映射类型来实现的,本节让我们看看什么是映射类型。

获取对象类型某个字段的类型

在介绍映射类型前,容我补充一个知识点——获取对象类属性的类型

TS类型不能向JS那样用点.来获取属性,但是可以使用中括号,例如:

元组

type T = [number, boolean, string]

type T1 = T[1] // boolean - 获取数组某一项的类型
type Len = T['length'] // 3 - 获取数组的长度
type All = T[number] // number | boolean | string - 相当于将元组类型转换为联合类型

对象

type P = {
    x: 1
    y: 8
}

type X = P['x'] // 1
type Key = P[keyof P] // 1 | 8
type Key = 'name' | 'age'
type User = {
    name: string
    age: number
}

type Value = User[Key] // type Value = string | number

基本使用

type K = 'name' | 'age'
type R = {
    [P in K]: P
}
//--------------------------------------------
// 结果:
type R = {
    name: 'name'
    age: 'age'
}

可以看到这里使用的in操作符,它可以将一个联合类型映射为对象类型的属性,P in K,其中K必须是联合类型string | number | symbol的子集。

使用映射类型复制一个对象类型:

type A = {
    0: boolean
    a: string
    b: number
}
type T = {
    [P in keyof A]: A[P]
}
//--------------------------------------------
// 结果
type T = {
    0: boolean
    a: string
    b: number
}

当然,我们完全没必要用这种方式来复制一个对象类型,这里只是为了举例说明in的使用方法以及它的作用。

映射类型使用断言

in的子句后面可以添加as断言,例如上面复制的例子,我们可以给它添加断言:

type A = {
    0: boolean
    a: string
    b: number
}
type T = {
    [P in keyof A as string]: A[P] // 断言为string
}
//--------------------------------------------
// 结果
type T = {
    [x: string]: string | number | boolean
}

在这个例子中,断言将类型扩大了,原本keyof A应该是 0 | 'a' | 'b',断言后扩大成了string

我们再看这个例子:

type A = {
    0: boolean
    a: string
    b: number
}

type T = {
    [P in keyof A as P extends string ? P : never]: A[P]
}
//--------------------------------------------
// 结果
type T = {
    a: string
    b: number
}

在这个例子中,P extends string ? P : never的意思是:如果P是string,就保留P这个属性,否则就不要这个属性(never)。于是,结果就只剩ab两个属性了。

断言可以扩大类型,也可以缩小类型,我们甚至可以断言原本不存在的属性:

type A = {
    0: boolean
    a: string
    b: number
}

type T = {
    [P in keyof A as 'c']: A[P]
}
//--------------------------------------------
// 结果
type T = {
    c: string | number | boolean
}

可以看到,keyof A原本并没有c这个属性,断言凭空指定了它,只要它是 string | number | symbol的子集 即可。

映射类型实现的TS内置工具类

下面我们来看一看通过映射类型实现的TS内置工具类。

其实,只要你弄懂了映射类型的用法,这些工具类的实现都是很简单的。

Partial

Partial会将对象类型的所有属性都变为可选属性

type Partial<T> = {
    [P in keyof T]?: T[P] | undefined
} 

复制 + 问号

Required

Required会将对象类型的所有属性都变为必须属性

type Required<T> = {
    [P in keyof T]-?: T[P]
} 

复制-问号

Readonly

Readonly会将对象类型的所有属性变为只读

type Readonly<T> = {
    readonly [P in keyof T]: T[P]
} 

复制并只读

Record

Record的作用是创建一个指定键、值类型的对象类型。

type List = Record<'a' | 'b' | 'c', string>
//--------------------------------------------
// 结果:
type List = {
    a: string
    b: string
    c: string
}

Record的实现:

type Record<K extends string | number | symbol, T> = { 
    [P in K]: T
}

由于K必须是string | number | symbol的子集,所以必须有泛型约束,否则会报错:

type MyRecord<K, T> = { 
    [P in K]: T // 类型错误:不能将类型“K”分配给类型“string | number | symbol”。ts(2322)
}

Pick

Pick用于提取对象中指定的属性,得到一个新的对象类型。

type T = Pick<{ a: 1; b: 2; c: 3 }, 'a' | 'c'>
//--------------------------------------------
// 结果:
type T = { a: 1; c: 3 }

它的实现十分简单:

type Pick<T, K extends keyof T> = { 
    [P in K]: T[P]
}

Omit

OmitPick的作用正好相反,它用于排除对象类型的指定属性。

type T = Omit<{ a: 1; b: 2; c: 3 }, 'a' | 'c'>
//--------------------------------------------
// 结果:
type T = { b: 2 }

它的实现需要借助Exclude

type Omit<T, K extends string | number | symbol> = { 
    [P in Exclude<keyof T, K>]: T[P]
}

结束

本来打算再多介绍几个工具类,但涉及的知识点有点多,全塞一篇文章里有点太多了,ReturnType Parameters等等,这些就留到下一篇吧。