12-TypeScript 内置的高级类型有哪些?
课程
1
如何阅读本小册
已学完
学习时长: 2分6秒
2
为什么说 TypeScript 的火爆是必然?
已学完
学习时长: 4分28秒
3
TypeScript 类型编程为什么被叫做类型体操?
已学完
学习时长: 9分16秒
4
TypeScript 类型系统支持哪些类型和类型运算?
已学完
学习时长: 19分27秒
5
套路一:模式匹配做提取
已学完
学习时长: 41分32秒
6
套路二:重新构造做变换
已学完
学习时长: 39分51秒
7
套路三:递归复用做循环
学习时长: 42分22秒
8
套路四:数组长度做计数
学习时长: 27分58秒
9
套路五:联合分散可简化
学习时长: 19分36秒
10
套路六:特殊特性要记清
学习时长: 36分41秒
11
类型体操顺口溜
学习时长: 27分26秒
12
TypeScript 内置的高级类型有哪些?
学习时长: 31分59秒
13
真实案例说明类型编程的意义
学习时长: 34分36秒
14
类型编程综合实战一
学习时长: 25分55秒
15
类型编程综合实战二
学习时长: 29分9秒
16
新语法 infer extends 是如何简化类型编程的
学习时长: 11分14秒
17
原理篇:逆变、协变、双向协变、不变
学习时长: 11分31秒
18
原理篇:编译 ts 代码用 tsc 还是 babel?
学习时长: 19分49秒
19
原理篇:实现简易 TypeScript 类型检查
学习时长: 47分51秒
20
原理篇:如何阅读 TypeScript 源码
学习时长: 19分27秒
21
原理篇:一些特殊情况的说明
已学完
学习时长: 12分39秒
22
小册总结
学习时长: 2分32秒
23
加餐:3 种类型来源和 3 种模块语法
学习时长: 17分6秒
24
加餐:用 Project Reference 优化 tsc 编译性能
已学完
学习时长: 4分47秒
juejin_logo copyCreated with Sketch.

学完了 6 个类型体操的套路之后,各种类型编程逻辑我们都能写出来,但其实一些常见的类型不用自己写, TypeScript 内置了很多,这节我们就来看下 TypeScript 内置了哪些高级类型吧。

Parameters

Parameters 用于提取函数类型的参数类型。

源码是这样的:

type Parameters<T extends (...args: any) => any> 
    = T extends (...args: infer P) => any 
        ? P 
        : never;

类型参数 T 为待处理的类型,通过 extends 约束为函数,参数和返回值任意。

通过 extends 匹配一个模式类型,提取参数的类型到 infer 声明的局部变量 P 中返回。

这样就实现了函数参数类型的提取:

试一下

这就是个简单的模式匹配,学完套路一轻轻松松就写出来了。

ReturnType

ReturnType 用于提取函数类型的返回值类型。

源码是这样的:

type ReturnType<T extends (...args: any) => any> 
    = T extends (...args: any) => infer R 
        ? R 
        : any;

类型参数 T 为待处理的类型,通过 extends 约束为函数类型,参数和返回值任意。

用 T 匹配一个模式类型,提取返回值的类型到 infer 声明的局部变量 R 里返回。

这样就实现了函数返回值类型的提取:

试一下

和提取函数参数类型差不多,也是个简单的模式匹配。

ConstructorParameters

构造器类型和函数类型的区别就是可以被 new。

Parameters 用于提取函数参数的类型,而 ConstructorParameters 用于提取构造器参数的类型。

源码是这样的:

type ConstructorParameters<
    T extends abstract new (...args: any) => any
> = T extends abstract new (...args: infer P) => any 
    ? P 
    : never;

类型参数 T 是待处理的类型,通过 extends 约束为构造器类型,加个 abstract 代表不能直接被实例化(其实不加也行)。

用 T 匹配一个模式类型,提取参数的部分到 infer 声明的局部变量 P 里,返回 P。

这样就实现了构造器参数类型的提取:

试一下

构造器参数的提取依然是模式匹配。

InstanceType

提取了构造器参数的类型,自然也可以提取构造器返回值的类型,就是 InstanceType。

源码是这样的:

type InstanceType<
    T extends abstract new (...args: any) => any
> = T extends abstract new (...args: any) => infer R 
    ? R 
    : any;

整体和 ConstructorParameters 差不多,只不过提取的不再是参数了,而是返回值。

通过模式匹配提取返回值的类型到 infer 声明的局部变量 R 里返回。

这样就实现了构造器的实例类型的提取:

试一下

ThisParameterType

函数里可以调用 this,这个 this 的类型也可以约束:

同样,this 的类型也可以提取出来,通过 ThisParameterType 这个内置的高级类型:

它的源码是这样的:

type ThisParameterType<T> = 
    T extends (this: infer U, ...args: any[]) => any 
        ? U 
        : unknown;

类型参数 T 为待处理的类型。

用 T 匹配一个模式类型,提取 this 的类型到 infer 声明的局部变量 U 里返回。

这样就实现了 this 类型的提取。

试一下

OmitThisParameter

提取出 this 的类型之后,自然可以构造一个新的,比如删除 this 的类型可以用 OmitThisParameter。

它的源码是这样的:

type OmitThisParameter<T> = 
    unknown extends ThisParameterType<T> 
        ? T 
        : T extends (...args: infer A) => infer R 
            ? (...args: A) => R 
            : T;

类型参数 T 为待处理的类型。

用 ThisParameterType 提取 T 的 this 类型,如果提取出来的类型是 unknown 或者 any,那么 unknown extends ThisParameterType 就成立,也就是没有指定 this 的类型,所以直接返回 T。

否则,就通过模式匹配提取参数和返回值的类型到 infer 声明的局部变量 A 和 R 中,用它们构造新的函数类型返回。

这样,就实现了去掉 this 类型的目的:

试一下

这个类型除了模式匹配做提取外,也用到了重新构造做变换,稍微复杂一些。

Partial

索引类型可以通过映射类型的语法做修改,比如把索引变为可选。

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

类型参数 T 为待处理的类型。

通过映射类型的语法构造一个新的索引类型返回,索引 P 是来源于之前的 T 类型的索引,也就是 P in keyof T,索引值的类型也是之前的,也就是 T[P]。

这样就实现了把索引类型的索引变为可选的效果:

试一下

Required

可以把索引变为可选,也同样可以去掉可选,也就是 Required 类型:

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

类型参数 T 为待处理的类型。

通过映射类型的语法构造一个新的索引类型,索引取自之前的索引,也就是 P in keyof T,但是要去掉可选,也就是 -?,值的类型也是之前的,就是 T[P]。

这样就实现了去掉可选修饰的目的:

试一下

Readonly

同样的方式,也可以添加 readonly 的修饰:

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

类型参数 T 为待处理的类型。

通过映射类型的语法构造一个新的索引类型返回,索引和值的类型都是之前的,也就是 P in keyof T 和 T[P],但是要加上 readonly 的修饰。

这样就实现了加上 readonly 的目的:

试一下

Pick

映射类型的语法用于构造新的索引类型,在构造的过程中可以对索引和值做一些修改或过滤。

比如可以用 Pick 实现过滤:

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

类型参数 T 为待处理的类型,类型参数 K 为要过滤出的索引,通过 extends 约束为只能是 T 的索引的子集。

构造新的索引类型返回,索引取自 K,也就是 P in K,值则是它对应的原来的值,也就是 T[P]。

这样就实现了过滤的目的:

试一下

Record

Record 用于创建索引类型,传入 key 和值的类型:

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

这里很巧妙的用到了 keyof any,它的结果是 string | number | symbol:

image.png

但如果你开启了 keyOfStringsOnly 的编译选项,它就只是 stirng 了:

用 keyof any 是动态获取的,比直接写死 string | number | symbol 更好。

继续讲 Record 这个类型,它用映射类型的语法创建了新的索引类型,索引来自 K,也就是 P in K,值是传入的 T。

这样就用 K 和 T 构造出了对应的索引类型。

当传入的 K 是 string | number | symbol,那么创建的就是有可索引签名的索引类型:

试一下

Exclude

当想从一个联合类型中去掉一部分类型时,可以用 Exclude 类型:

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

联合类型当作为类型参数出现在条件类型左边时,会被分散成单个类型传入,这叫做分布式条件类型。

所以写法上可以简化, T extends U 就是对每个类型的判断。

过滤掉 U 类型,剩下的类型组成联合类型。也就是取差集。

试一下

这里用了分布式条件类型的性质,写法上可以简化。

Extract

可以过滤掉,自然也可以保留,Exclude 反过来就是 Extract,也就是取交集:

type Extract<T, U> = T extends U ? T : never;

试一下

Omit

我们知道了 Pick 可以取出索引类型的一部分索引构造成新的索引类型,那反过来就是去掉这部分索引构造成新的索引类型。

可以结合 Exclude 来轻松实现:

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

类型参数 T 为待处理的类型,类型参数 K 为索引允许的类型(string | number | symbol 或者 string)。

通过 Pick 取出一部分索引构造成新的索引类型,这里用 Exclude 把 K 对应的索引去掉,把剩下的索引保留。

这样就实现了删除一部分索引的目的:

试一下

Awaited

在递归那节我们写过取 Promise 的 ValuType 的高级类型,这个比较常用,ts 也给内置了,就是 Awaited。

它的实现比我们当时写的完善一些:

type Awaited<T> =
    T extends null | undefined
        ? T 
        : T extends object & { then(onfulfilled: infer F): any }
            ? F extends ((value: infer V, ...args: any) => any)
                ? Awaited<V>
                : never 
            : T;

类型参数 T 是待处理的类型。

如果 T 是 null 或者 undefined,就返回 T。

如果 T 是对象并且有 then 方法,那就提取 then 的参数,也就是 onfulfilled 函数的类型到 infer 声明的局部变量 F。

继续提取 onfullfilled 函数类型的第一个参数的类型,也就是 Promise 返回的值的类型到 infer 声明的局部变量 V。

递归的处理提取出来的 V,直到不再满足上面的条件。

这样就实现了取出嵌套 Promise 的值的类型的目的:

为什么要提取 then 方法的第一个参数的返回值类型看下 Promise 的结构就明白了:

new Promise(() => {
    // xxx
}).then((value) => {
});

then 第一个参数是 onfullfilled 的回调,从它的第一个参数就能拿到返回的值的类型。

对比下我们之前的实现:

type DeepPromiseValueType2<T> = 
    T extends Promise<infer ValueType> 
        ? DeepPromiseValueType2<ValueType>
        : T;

内置的高级类型不再限制必须是 Promise,而是只要对象且有 then 方法就可以,这样更通用了一些。

试一下

NonNullable

NonNullable 就是用于判断是否为非空类型,也就是不是 null 或者 undefined 的类型的,实现比较简单:

type NonNullable<T> = T extends null | undefined ? never : T;

当传入 null 时:

当传入非空类型时:

试一下

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 是固有的意思,就像 js 里面的有的方法打印会显示 [native code] 一样。这部分类型不是在 ts 里实现的,而是编译过程中由 js 实现的。

我们可以在源码里找到对应的处理代码:

其实就是 ts 编译器处理到这几个类型时就直接用 js 给算出来了。

为啥要这样做呢?

因为快啊,解析类型是要处理 AST 的,性能比较差,用 js 直接给算出来那多快呀。

这几个类型的原理在原理篇也会带大家 debug 下源码。

试一下

这基本就是全部的内置高级类型了。

总结

虽然我们学完 6 个套路,各种类型编程逻辑都能写了,但是常用的类型 TS 已经内置了。

这些内置的高级类型用我们学的套路很容易可以实现。

比如用模式匹配可以实现:Parameters、ReturnType、ConstructorParameters、InstanceType、ThisParameterType。

用模式匹配 + 重新构造可以实现:OmitThisParameter

用重新构造可以实现:Partial、Required、Readonly、Pick、Record

用模式匹配 + 递归可以实现: Awaited

用联合类型在分布式条件类型的特性可以实现: Exclude

此外还有 NonNullable 和四个编译器内部实现的类型:Uppercase、Lowercase、Capitalize、Uncapitalize。

这些类型也不咋需要记,就算忘记了自己也能很快的实现。重点还是放在 6 个类型编程的套路上。

本文的案例合并

留言
Ctrl + Enter
全部评论(36)
L就是我的头像
删除
这个Awaited 实现为什么会报错
Type 'Awaited' is not generic.
点赞
回复
三郎mr的头像
删除
居然全看懂了,感谢分享,干货满满
点赞
1
删除
(作者)
[碰拳]
点赞
回复
kzkz的头像
删除
光神,为什么Awaited 类型实现中要单独把 null 和 undefined 拎出来判断呢?
1
2
删除
是不是 通过 T extends null | undefined 及早结束 运算逻辑,提高性能?
T extends null | undefined 的运算相比T extends object & { then(onfulfilled: infer F): any } 运算要多耗性能。
点赞
回复
删除
有道理
是不是 通过 T extends null | undefined 及早结束 运算逻辑,提高性能?
T extends null | undefined 的运算相比T extends object & { then(onfulfilled: infer F): any } 运算要多耗性能。
点赞
回复
bubbletg的头像
删除
haishiHAISH还是得多写,看完了,就看完了,啥也没记住。哈哈哈
1
1
删除
确实
点赞
回复
爱吃鱼的桶哥Z的头像
删除
伪 · 全栈打杂攻城狮
打卡
点赞
回复
零式大人的头像
删除
普通上班族
Omit 的 `试一下` 是 NonNullable 的案例
点赞
1
删除
(作者)
我改改
点赞
回复
ShidongZhao的头像
删除
前端开发
光之呼吸
点赞
回复
树洞的头像
删除
自动匿名机器人 @ 自动匿名机器人
是不是记住这章就差不多了,不用自己做体操
点赞
1
删除
(作者)
常用的是这些,但是很多情况下,还是很多时候还是要自己写,下一节会讲一些真实的例子
点赞
回复
宁静致远本尊63688的头像
删除
ts内置的DOM类型,Date类型等等,能出个章节让学习下,自己写demo的时候,发现是高频用到,但是找不到好的学习资料
4
回复
sufuwang的头像
删除
学生
Extract 类型,为什么不直接写第二个参数呀?
type a = Extract<'a'|'b', 'a'> 为什么不直接写 type a = 'a' ???
点赞
4
删除
(作者)
这里只是举个例子,实际上传入啥可能是动态算出来的
点赞
回复
删除
明白作者是想举个例子,我理解 Extract 是将右边的类型从左边剔除出来,那为什么不能直接写右边的类型呢?可以举一个这个类型工具存在的意义吗
这里只是举个例子,实际上传入啥可能是动态算出来的
点赞
回复
查看更多回复
前端家里蹲的头像
删除
前端开发工程师 @ 家里蹲
OmitThisParameter 重新构造一个类型不太理解, 为什么 (...args: infer P) => infer R, 它的参数应该是两个,包含了this, 重新构造后就 去除了 this?
点赞
2
删除
(作者)
是的,新构造的函数类型没有包含 this 的类型
点赞
回复
删除
这里要参照一下,解构赋值,args 是一个固定的内置参数名吧;上面还有另一个固定的this, 所以 拿到的P 里面没有this
点赞
回复
nxl的头像
删除
前端开发
所有内置都在这里吗
点赞
1
删除
(作者)
对的,差不多就这些
点赞
回复
是心动ya的头像
删除
菜狗
光大大, 是不是手打快了
点赞
1
删除
(作者)
我改改
点赞
回复
dream的头像
删除
前端工程师
打卡
点赞
回复
kenshin的头像
删除
OmitThisParameter这个内置类型实现不太明白,T extends (...args: infer A) => infer R
? (...args: A) => R
: T;这里为啥能把this给过滤掉啊
1
5
删除
(作者)
因为重新构造了一个新的,没有加上 this 类型呀
点赞
回复
删除
我也没太理解。
class Person {
name: string
getName(this: Person, a: string){
return 'test' + this.name
}
}
let person = new Person()
type OmitThisParameterTest = OmitThisParameter<typeof person.getName>
这个时候 (...args: infer A) 不应该是有两个参数组成的数组嘛 [this, a], 然后 A 不应该是 [this, a], 为啥就过滤掉了呢?
展开
因为重新构造了一个新的,没有加上 this 类型呀
点赞
回复
查看更多回复
阿五的头像
删除
前端打杂 @ 「程序员菜馆」公众号作者
小册过半了,学了很多
点赞
1
删除
(作者)
[碰拳]
点赞
回复