typescript 类型工具
# 0. 前言
本篇博客主要是对 Typescript
内置类型做一个梳理和总结。
TypeScript
官网内置类型工具见:https://www.typescriptlang.org/docs/handbook/utility-types.html
# 1. 什么是工具类型
在 ts
中,我们可以使用 type
或者 interface
进行类型约束,这些类型一旦写死,后期只能通过 &
或者 |
的方式进行扩展,而借助工具类型可实现更灵活的类型创建功能。
我们可以将工具类型类比函数:
真实的函数:入参和出参都是具体的值。
工具类型:以泛型的方式传入类型,返回结果是一个新的类型。
如,下面这个例子可在原有的
string
和number
数据类型基础上,再支持一种类型。type Factory<T> = T | number | string;
1
在 Typescript
内置了许多的工具类型,以实现常见类型的转换。并且这些工具类型是作用在全局的,可以直接使用。但是有一点,在使用时需确定注意 ts
版本。
注:我们可以在全局直接使用的原因,内置工具类型被定义在
node_modules/typescripts/lib/lib.es5.d.ts
文件中。
# 2. Ts
中内置工具类型
# 0. 函数类型相关
Parameters
(v3.1)ReturnType
(v2.8)
如果存在如下函数:
function add(num1:number,num2:number)=>{
return num1 + num2;
}
2
3
通过内置类型,我们可以:
// 1. 通过 typeof 获取 function 的 ts 定义
type addFunType = typeof add; // (num1:number,num2:number) => number
// 2. 通过 Parameters 获取入参类型
type InputType = Parameters<typeof fun>[0];
// 3. 通过 ReturnType 获取出参类型
type outputType = ReturnType<typeof fun>;
2
3
4
5
6
7
8
此类函数在 React
组件中非常有用,无论是 Class Component
还是 Function Component
本质上都是一个函数,通过 Parameters
可以快速拿到组件的 props
类型,如下:
// 提取 ComponentA 组件的 PropsType
type ComponetAPropsType = Parameters<typeof ComponentA>[0];
// 有了此方法,定义 HOC 就不必再重复写一次 Props 的类型,而是可以通过组件自身推断出来。
const ComponentAWithHeader = AddHeaderHoc<ComponetAPropsType>(ComponentA);
2
3
4
5
此技巧来源于:字节跳动:Larry (opens new window)
不使用工具类型的写法如下:
type Parameters<K extends (...args:any) => any> = K extends(...args: infer P) =>any => any ? P : never
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
2
核心原理:使用到了 infer
+ extends
(类型判断)
# 1. 类型相关
Extract<Type, Union>
(2.8):提取公共类型Exclude<UnionType, ExcludedMembers>
(2.8):排除指定类型
使用说明如下,如存在一个联合对象:
type objType = string | number | (() => void);
通过内置类型,我们可以做到
// 1. 提取公共类型, 如提取出所有的function
type T0 = Extract<string | number | (() => void), Function>;
// 2. 或者排除掉 string 和 number 类型
type T0 = Exclude<string | number | (() => void,string | number>;
2
3
4
5
尽管使用方式非常简单,但是底层实现机制还是有一定难度的,需要对 extends
这个关键字的原理非常清楚,代码也非常少:
type Extract<T, U> = T extends U ? T : never;
type Exclude<T, U> = T extends U ? never : T;
2
在条件类型中会使用 extends
去判断类型的兼容性,但是会存在一个细节点,先看下面的这个例子:
type Condition<T> = T extends 1 | 2 | 3 ? T : never;
// 直接判断,结果显然是 never 没啥问题。
type Res2 = 1 | 2 | 3 | 4 | 5 extends 1 | 2 | 3 ? 1 | 2 | 3 | 4 | 5 : never;
// 1 | 2 | 3 (联合类型这里会实现了一个广播的效果)
type Res1 = Condition<1 | 2 | 3 | 4 | 5>;
2
3
4
5
6
7
当 联合类型
以泛型参数的方式传入时,原有的条件类型判断会发生改变,不再是对两个类型进行直接判断(前者是否兼容后者),而是以一种广播(automativally distributed
)的方式对联合类型逐个进行比较,并将结果进行合并。即输入联合,输出联合。
PS:官方的说法是,这是条件类型的分布式特性,并且定义了一个裸类型参数的概念。
Conditional types in which the checked type is a naked type parameter are called distributive conditional types. Distributive conditional types are automatically distributed over union types during instantiation.
因此这里的内置类型也是利用到的 extend
+ Union Type
的 auto distribute
特性。
如果禁用此特性,可以使用以下方案:
// 方案1:
type NoDistribute<T> = T & {};
// 方案2:使用数组包裹
type CompareUnion<T, U> = [T] extends [U] ? true : false;
2
3
4
# 2. 对象相关
Record<Keys,Type>
(v2.8)Omit<Type, Keys>
(3.5)Pick<Type, Keys>
(2.1)Partial<Type>
(2.1)Required<Type>
(2.8)Readonly<Type>
(2.1)- 自定义属性
clone
对于对象内置工具类型而言,通过简单的类型映射就可以实现,内置更多的是一种简化操作。
使用说明,当存在一个对象如下:
interface Todo {
title: string;
description?: string;
completed: boolean;
createdAt: number;
}
2
3
4
5
6
通过内置类型,我们可以做到:
// 1. 将 Todo 的所有属性设置为必选,或者可选
type T0 = Required<Todo>;
type T1 = Partial<Todo>;
type T3 = ReadOnly<Todo>;
// 2. 筛选或者丢弃掉多个属性
type T3 = Pick<Todo, "title" | "description">;
type T4 = Omit<Todo, "title" | "description">;
// 3. 使用 record 创建对象
type Todo2 = Record<"title" | "description", string> &
Record<"completed", boolean> &
Record<"createdAt", number>;
// 4. 提取 key 与 value 的联合类型
type keyType = keyof Todo; // "title" | "description" | "completed" | "createAt"
type valueType = Todo[keyof Todo]; // string | number | boolean | undefined
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
各自内部的代码实现,核心是 keyof
提取出索引签名
实现的:
// keyof 是联合变量,联合变量在 ts 中可通过 [ P in 联合类型 ] 的形式遍历
// 与 extends 关键字类型,此处联合类型会被自动分发
/* 假设我们实现一个 clone 接口,用于拷贝一个 Ts 类型*/
type clone<T> = {
[P in keyof T]: T[P];
};
/* 只需在 clone 基础上稍作修改可以封装出 Partial 和 Require 类型*/
type Required<T> = {
[P in keyof T]-?: T[P];
};
type Partial<T> = {
[P in keyof T]?: T[P];
};
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
/* Record 就是上面的简化版*/
type Record<K extends keyof any, T> = {
[P in K]: T;
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 3.Promise 类型
在 Typescript
的 4.5
版本中,新增了 Awaited<Type>
类型
使用方案如下:
type A = Awaited<Promise<string>>;
// 即使嵌套也可以使用
type B = Awaited<Promise<Promise<string>>>; // string
// 联合类型也适用
type C = Awaited<boolean | Promise<number>>; // number | boolean
2
3
4
5
核心原理如下:(没看懂)
type Awaited<T> = T extends null | undefined
? T // special case for `null | undefined` when not in `--strictNullChecks` mode
: T extends object & { then(onfulfilled: infer F): any } // `await` only unwraps object types with a callable `then`. Non-object types are not unwrapped
? F extends (value: infer V, ...args: any) => any // if the argument to `then` is callable, extracts the first argument
? Awaited<V> // recursively unwrap the value
: never // the argument to `then` was not callable
: T; // non-object or non-thenable
2
3
4
5
6
7
# 3. 自定义工具类型
# Maybe + 类型
// 扩展类型
type Factory<T> = T | number | string;
// 有可能为 null
type MaybeNull<T> = T | null;
// 有可能为 Array
type MaybeArray<T> = T | T[];
2
3
4
5
6
7
8
# 解决裸参数问题
NoDistribute
CompareUnion
:在泛型中比较联合类型
// 禁止联合类型自动分发
type NoDistribute<T> = T & {};
// 比较两个类型
type CompareUnion<T, U> = [T] extends [U] ? true : false;
2
3
4
5
测试如下:
// 测试1:
type Wrapped<T> = NoDistribute<T> extends boolean ? "Y" : "N";
type Res1 = Wrapped<number | boolean>; // "N"
type Res2 = Wrapped<true | false>; // "Y"
type Res3 = Wrapped<true | false | 599>; // "N"
// 测试2:
type CompareRes1 = CompareUnion<1 | 2, 1 | 2 | 3>; // true
type CompareRes2 = CompareUnion<1 | 2, 1>; // false
2
3
4
5
6
7
8
9