Search K
Appearance
Appearance
学完了 6 个类型体操的套路之后,各种类型编程逻辑我们都能写出来,但其实一些常见的类型不用自己写, TypeScript 内置了很多,这节我们就来看下 TypeScript 内置了哪些高级类型吧。
Parameters 用于提取函数类型的参数类型。
源码是这样的:
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;类型参数 T 为待处理的类型,通过 extends 约束为函数,参数和返回值任意。
通过 extends 匹配一个模式类型,提取参数的类型到 infer 声明的局部变量 P 中返回。
这样就实现了函数参数类型的提取:

这就是个简单的模式匹配,学完套路一轻轻松松就写出来了。
ReturnType 用于提取函数类型的返回值类型。
源码是这样的:
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;类型参数 T 为待处理的类型,通过 extends 约束为函数类型,参数和返回值任意。
用 T 匹配一个模式类型,提取返回值的类型到 infer 声明的局部变量 R 里返回。
这样就实现了函数返回值类型的提取:

和提取函数参数类型差不多,也是个简单的模式匹配。
构造器类型和函数类型的区别就是可以被 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。
源码是这样的:
type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R
? R
: any;整体和 ConstructorParameters 差不多,只不过提取的不再是参数了,而是返回值。
通过模式匹配提取返回值的类型到 infer 声明的局部变量 R 里返回。
这样就实现了构造器的实例类型的提取:

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

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

它的源码是这样的:
type ThisParameterType<T> = T extends (this: infer U, ...args: any[]) => any ? U : unknown;类型参数 T 为待处理的类型。
用 T 匹配一个模式类型,提取 this 的类型到 infer 声明的局部变量 U 里返回。
这样就实现了 this 类型的提取。
提取出 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 类型的目的:

这个类型除了模式匹配做提取外,也用到了重新构造做变换,稍微复杂一些。
索引类型可以通过映射类型的语法做修改,比如把索引变为可选。
type Partial<T> = {
[P in keyof T]?: T[P];
};类型参数 T 为待处理的类型。
通过映射类型的语法构造一个新的索引类型返回,索引 P 是来源于之前的 T 类型的索引,也就是 P in keyof T,索引值的类型也是之前的,也就是 T[P]。
这样就实现了把索引类型的索引变为可选的效果:

可以把索引变为可选,也同样可以去掉可选,也就是 Required 类型:
type Required<T> = {
[P in keyof T]-?: T[P];
};类型参数 T 为待处理的类型。
通过映射类型的语法构造一个新的索引类型,索引取自之前的索引,也就是 P in keyof T,但是要去掉可选,也就是 -?,值的类型也是之前的,就是 T[P]。
这样就实现了去掉可选修饰的目的:

同样的方式,也可以添加 readonly 的修饰:
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};类型参数 T 为待处理的类型。
通过映射类型的语法构造一个新的索引类型返回,索引和值的类型都是之前的,也就是 P in keyof T 和 T[P],但是要加上 readonly 的修饰。
这样就实现了加上 readonly 的目的:

映射类型的语法用于构造新的索引类型,在构造的过程中可以对索引和值做一些修改或过滤。
比如可以用 Pick 实现过滤:
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};类型参数 T 为待处理的类型,类型参数 K 为要过滤出的索引,通过 extends 约束为只能是 T 的索引的子集。
构造新的索引类型返回,索引取自 K,也就是 P in K,值则是它对应的原来的值,也就是 T[P]。
这样就实现了过滤的目的:

Record 用于创建索引类型,传入 key 和值的类型:
type Record<K extends keyof any, T> = {
[P in K]: T;
};这里很巧妙的用到了 keyof any,它的结果是 string | number | symbol:

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


用 keyof any 是动态获取的,比直接写死 string | number | symbol 更好。
继续讲 Record 这个类型,它用映射类型的语法创建了新的索引类型,索引来自 K,也就是 P in K,值是传入的 T。
这样就用 K 和 T 构造出了对应的索引类型。

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

当想从一个联合类型中去掉一部分类型时,可以用 Exclude 类型:
type Exclude<T, U> = T extends U ? never : T;联合类型当作为类型参数出现在条件类型左边时,会被分散成单个类型传入,这叫做分布式条件类型。
所以写法上可以简化, T extends U 就是对每个类型的判断。
过滤掉 U 类型,剩下的类型组成联合类型。也就是取差集。

这里用了分布式条件类型的性质,写法上可以简化。
可以过滤掉,自然也可以保留,Exclude 反过来就是 Extract,也就是取交集:
type Extract<T, U> = T extends U ? T : never;
我们知道了 Pick 可以取出索引类型的一部分索引构造成新的索引类型,那反过来就是去掉这部分索引构造成新的索引类型。
可以结合 Exclude 来轻松实现:
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;类型参数 T 为待处理的类型,类型参数 K 为索引允许的类型(string | number | symbol 或者 string)。
通过 Pick 取出一部分索引构造成新的索引类型,这里用 Exclude 把 K 对应的索引去掉,把剩下的索引保留。
这样就实现了删除一部分索引的目的:

在递归那节我们写过取 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 就是用于判断是否为非空类型,也就是不是 null 或者 undefined 的类型的,实现比较简单:
type NonNullable<T> = T extends null | undefined ? never : T;当传入 null 时:

当传入非空类型时:

这四个类型是分别实现大写、小写、首字母大写、去掉首字母大写的。




它们的源码时这样的:
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 个类型编程的套路上。