本篇介绍标题中提到的三个关键词。笼统来说,它们的作用大体上可以简介为:
- Utility Types 中文叫 “实用类型”,它们是由 TypeScript 内置提供的一系列类型工具,用于实现一些类型操作;
intrinsic
是表示 “占位” 意义的关键词,有些复杂类型必须由类型系统内部来实现,所以放这么一个关键词来占位;infer
表示 “推断”,可以理解为中学时期数学课解方程时使用的 “未知数”,也就是把某个类型当做 “未知数”。
Utility Types 实用类型
以 VSCode 为例,在任一个 .ts 文件中随便输入一个实用类型,例如 Omit
,然后左键点击并按下 F12,即可打开当前支持的所有实用类型的源码文件。这里可以看到所有内置的实用类型以及它们的实现源代码。
这个文件中也包含了当前环境下的很多全局变量、全局 API 的类型。
以下是各实用类型的介绍:
字段可空性和访问性处理:
Partial<T>
:使对象T
的所有字段变为非必须(可空);Required<T>
:所有字段为必须(不可空);Readonly<T>
:所有字段变为只读(添加readonly
修饰符)。
这里的
Partial
和Required
用到了+?
、-?
的映射属性修饰符语法。我们也可以使用
-readonly
修饰语法,来自己实现一个Readable
以实现与上面的Readonly
相反的功能:// 把对象所有字段变为可写 type Readable<T> = { -readonly [P in keyof T]: T[P] }
类型集合操作:
Extract<T, U>
:从集合T
中选取符合U
的所有项,用选取的项组成一个新集合;Exclude<T, U>
:从集合T
中删去符合U
的所有项,用剩余的项组成一个新集合;NonNullable<T>
:从集合T
中删去所有null
和undefined
的项,用剩余的项组成一个新集合。
对象字段:
Pick<T, K>
:从对象T
中选取数个符合K
的字段,选取的字段组成一个新对象;Omit<T, K>
:从对象T
中删去数个符合K
的字段,剩余的字段组成一个新对象;Record<K, V>
:得到一个对象,它所有的键都来自于K
,所有的值都是来自于V
。
实用类型
Pick
就像lodash
的_.pick
,而Omit
就像_.omit
;
实际上Omit
是由Pick
和Exclude
组合而来。
方法:
Parameters<T>
:通常配合typeof
关键字使用,获取方法T
的参数的类型,并组成一个数组返回;ReturnType<T>
:通常配合typeof
关键字使用,获取方法T
的返回值类型。
面向对象:
ConstructorParameters<T>
:同上Parameters<T>
,只不过传入的T
需要使用typeof
接构造函数名(也就是类名);InstanceType
:同上ReturnType<T>
,只不过传入的T
需要使用typeof
接构造函数名(也就是类名);ThisType<T>
:返回一个对象,这个对象的方法中,this
的类型为T
。现在有更好的语法来取代他,举例:
type User = {
name: string
age: number
}
// 第一个对象,必须使用 ThisType<User>,否则下面的 this.age 会提示错误
const user1: ThisType<User> = {
name: 'user1',
sayHello() {
return `我叫${this.name},年龄${this.age}`
},
}
// 第二个对象
const user2 = {
name: 'user2',
// 注意这里的 this 是一个不存在的参数,它写在这里仅用于表示此方法中 this 的类型
// 实际上 sayHello 的方法签名是 (),不接受任何参数
sayHello(this: User) {
return `我叫${this.name},年龄${this.age}`
},
}
字符串大小写处理:
Uppercase<T>
:字符串T
的全大写格式;Lowercase<T>
:字符串T
的全小写格式;Capitalize<T>
:字符串T
的首字母大写格式;Uncapitalize<T>
:字符串T
的首字母小写格式。
通常 Uppercase
、Lowercase
的使用场景比较少,此处给出一个使用示例:
/** 将指定对象的所有键转为大写,并添加 REACT_APP_ 前缀 */
type ReactAppEnvType<T extends object> = {
[K in keyof T as `REACT_APP_${Uppercase<string & K>}`]: T[K]
}
// 原始对象
const envVarObject = {
apiRoot: '',
openAiKey: '',
}
// 经过 ReactAppEnvType 转化后
// 此对象的键必须使用此格式,更换前缀或大小写都会导致错误
const reactEnvVarObject: ReactAppEnvType<typeof envVarObject> = {
REACT_APP_APIROOT: '',
REACT_APP_OPENAIKEY: '',
}
而 Capitalize
和 Uncapitalize
使用场景会更多一点,给出一个使用示例:
/** 为指定对象中所有的键添加 getter 和 setter 方法 */
type ModelType<T extends object> = T & {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
} & {
[K in keyof T as `set${Capitalize<string & K>}`]: (p: T[K]) => void
}
// 原始对象
const user = {
name: '',
}
// 经过 ModelType 转化后的对象
// 此对象的键必须使用此格式,更换前缀或大小写都会导致错误
const userModel: ModelType<typeof user> = {
name: '',
getName: () => '',
setName: p => {},
}
以上便是这些实用类型的用法。
intrinsic
关键字
上文中讲到了 Uppercase
、Lowercase
、Capitalize
和 Uncapitalize
四个实用类型。
可以看到它们的源码:
// 注:源码中含有注释,此处为了简洁,删除了所有注释
type Uppercase<S extends string> = intrinsic;
type Lowercase<S extends string> = intrinsic;
type Capitalize<S extends string> = intrinsic;
type Uncapitalize<S extends string> = intrinsic;
这里便出现了 intrinsic
关键字,而且 intrinsic
作为关键字在源码中总共就出现了这四次。
可以看出,这四个实用类型很难用其它实用类型和 TypeScript 语法组合来实现,实际上它是由 TypeScript 语言内部直接实现的,所以此处没有源码。
也就是说 intrinsic
关键字只是起到一个占位的作用,这也是 官方的说法。
infer
关键字
前面说过,infer
实际上用于类型推断的功能。infer
必须和 extends
配合写成三目运算符的格式,用于推断出现在占位符位置的类型。
简单来说:
- 代码
infer X
表示在当前这个位置放置 “未知数”X
,让 TypeScript 来推断位于此处的X
的类型; - 因为需要推断,所以
infer
需要配合三目运算符,它表示如果此类型可推断和无法推断时的类型取值。
光是这么说很难去理解,这里给出几个代码示例:
举例1,已知一个数组,获取它的成员类型:
// 一般来说数组会写成 User[] 这种形式,那么想推测 User 的类型,则写成 (infer U)[]
// 使用 infer U 来作为 User 这个类型的占位符,让 TypeScript 来推测 U
// 代码如下:
type TypeofArray<T extends any[]> = T extends (infer U)[] ? U : never
举例2,已知一个 Promise
,获取它成功后的值:
// 显而易见,直接将 infer 放在 Promise 的尖括号里
type TypeofPromise<T> = T extends Promise<infer R> ? R : never
举例3,TypeScript 内置实用类型 ReturnType<T>
的实现:
// 一般来说方法会写成 () => T 这种形式,那么想推测 T 类型,则写成:
// (...args: any) => infer R ? R : any
// 使用 infer R 来作为返回值类型的占位符,让 TS 来推测 R
// 代码如下
type ReturnType<T> = T extends (...args: any) => infer R ? R : any
TypeScript 中,
infer
定义的 “未知数” 类型如果放置在函数参数的位置,它遵循逆变规则,例如推断方法参数时存在多个类型A
和B
,那么最终结果是A & B
(更具体的参数兼容较为抽象的参数);
如果放在数组、对象字段、函数返回值等位置,它遵循协变规则,此时若存在多个类型则最终结果为A | B
(较为抽象类型可用于接收更具体的返回值)。