typescript:内置工具类型

一、工具类型的分类

内置的工具类型按照类型操作的不同,其实也可以大致划分为这么几类:

  • 对属性的修饰,包括对象属性和数组元素的可选/必选、只读/可写。我们将这一类统称为属性修饰工具类型
  • 对既有类型的裁剪、拼接、转换等,比如使用对一个对象类型裁剪得到一个新的对象类型,将联合类型结构转换到交叉类型结构。我们将这一类统称为结构工具类型
  • 对集合(即联合类型)的处理,即交集、并集、差集、补集。我们将这一类统称为集合工具类型
  • 基于 infer 的模式匹配,即对一个既有类型特定位置类型的提取,比如提取函数类型签名中的返回值类型。我们将其统称为模式匹配工具类型
  • 模板字符串专属的工具类型,比如神奇地将一个对象类型中的所有属性名转换为大驼峰的形式。这一类当然就统称为模板字符串工具类型了。

二、 属性修饰工具类型

(1)这一部分的工具类型主要使用属性修饰映射类型索引类型相关(索引类型签名、索引类型访问、索引类型查询均有使用,因此这里直接用索引类型指代)。

(2) 在内置工具类型中,访问性修饰工具类型包括以下三位:

//将属性全部变成可选
type Partial<T> = {
    [P in keyof T]?: T[P];
};
//将属性全部变成必选
type Required<T> = {
    [P in keyof T]-?: T[P];
};
//将属性全部变成只读
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

 1、Partial 是?,即标记属性为可选,而 Required 则是 -?,相当于在原本属性上如果有 ? 这个标记,则移除它。

2、 Partial 也可以使用 +? 来显式的表示添加可选标记。

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

 3、Readonly 中也可以使用 +readonly

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

4、虽然 TypeScript 中并没有提供它的另一半,但参考 Required 其实我们很容易想到这么实现一个工具类型 Mutable,来将属性中的 readonly 修饰移除:

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

三、结构工具类型

(1) 这一部分的工具类型主要使用条件类型以及映射类型索引类型

(2)结构工具类型其实又可以分为两类,结构声明结构处理

(3)结构声明工具类型即快速声明一个结构,比如内置类型中的 Record

//创建结构:单个类型或联合类型映射到T
type Record<K extends keyof any, T> = {
    [P in K]: T;
};

1、使用 extends keyof any 标明,传入的 K 可以是单个类型,也可以是联合类型,而 T 即为属性的类型。

// 键名均为字符串,键值类型未知
type Record1 = Record<string, unknown>;
// 键名均为字符串,键值类型任意
type Record2 = Record<string, any>;
// 键名为字符串或数字,键值类型任意
type Record3 = Record<string | number, any>;

(4)Pick、Omit

//获取部分属性
//Pick 是保留这些传入的键
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};
//去除部分属性
//Omit 则是移除这些传入的键
//反向工具类型基于正向工具类型实现
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

 Exclude<A, B> 的结果就是联合类型 A 中不存在于 B 中的部分:

type Tmp1 = Exclude<1, 2>; // 1
type Tmp2 = Exclude<1 | 2, 2>; // 1
type Tmp3 = Exclude<1 | 2 | 3, 2 | 3>; // 1
type Tmp4 = Exclude<1 | 2 | 3, 2 | 4>; // 1 | 3

 Exclude<keyof T, K> 其实就是 T 的键名联合类型中剔除了 K 的部分。

四、集合工具类型

(1) 这一部分的工具类型主要使用条件类型、条件类型分布式特性。

(2) 对于两个集合来说,通常存在交集、并集、差集、补集这么几种情况,用图表示是这样的:

  • 并集,两个集合的合并,合并时重复的元素只会保留一份(这也是联合类型的表现行为)。
  • 交集,两个集合的相交部分,即同时存在于这两个集合内的元素组成的集合。
  • 差集,对于 A、B 两个集合来说,A 相对于 B 的差集即为 A 中独有而 B 中不存在的元素 的组成的集合,或者说 A 中剔除了 B 中也存在的元素以后,还剩下的部分
  • 补集,补集是差集的特殊情况,此时集合 B 为集合 A 的子集,在这种情况下 A 相对于 B 的差集 + B = 完整的集合 A

(1)交集与差集

//交集
type Extract<T, U> = T extends U ? T : never;
//差集
type Exclude<T, U> = T extends U ? never : T;

交集 Extract ,其运行逻辑是这样的:

type AExtractB = Extract<1 | 2 | 3, 1 | 2 | 4>; // 1 | 2

type _AExtractB =
  | (1 extends 1 | 2 | 4 ? 1 : never) // 1
  | (2 extends 1 | 2 | 4 ? 2 : never) // 2
  | (3 extends 1 | 2 | 4 ? 3 : never); // never

而差集 Exclude 也是类似,但需要注意的是,差集存在相对的概念,即 A 相对于 B 的差集与 B 相对于 A 的差集并不一定相同,而交集则一定相同。

type SetA = 1 | 2 | 3 | 5;

type SetB = 0 | 1 | 2 | 4;

type AExcludeB = Exclude<SetA, SetB>; // 3 | 5
type BExcludeA = Exclude<SetB, SetA>; // 0 | 4

type _AExcludeB =
  | (1 extends 0 | 1 | 2 | 4 ? never : 1) // never
  | (2 extends 0 | 1 | 2 | 4 ? never : 2) // never
  | (3 extends 0 | 1 | 2 | 4 ? never : 3) // 3
  | (5 extends 0 | 1 | 2 | 4 ? never : 5); // 5

type _BExcludeA =
  | (0 extends 1 | 2 | 3 | 5 ? never : 0) // 0
  | (1 extends 1 | 2 | 3 | 5 ? never : 1) // never
  | (2 extends 1 | 2 | 3 | 5 ? never : 2) // never
  | (4 extends 1 | 2 | 3 | 5 ? never : 4); // 4

 (2)并集与补集

// 并集
export type Concurrence<A, B> = A | B;

// 交集
export type Intersection<A, B> = A extends B ? A : never;

// 差集
export type Difference<A, B> = A extends B ? never : A;

// 补集
export type Complement<A, B extends A> = Difference<A, B>;

1、 补集基于差集实现,我们只需要约束集合 B 为集合 A 的子集即可。

 (3)内置工具类型中还有一个场景比较明确的集合工具类型:

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

type _NonNullable<T> = Difference<T, null | undefined>

很明显,它的本质就是集合 T 相对于 null | undefined 的差集,因此我们可以用之前的差集来进行实现。

五、 模式匹配工具类型

(1)这一部分的工具类型主要使用条件类型与 infer 关键字

infer是一个关键字,用于在条件类型(conditional types)中推断(infer)类型参数。

(2)  infer 其实代表了一种 模式匹配(pattern matching) 的思路,如正则表达式、Glob 中等都体现了这一概念。

(3)对函数类型签名的模式匹配:

type FunctionType = (...args: any) => any;

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

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

1、根据 infer 的位置不同,我们就能够获取到不同位置的类型,在函数这里则是参数类型与返回值类型。

2、我们还可以更进一步,比如只匹配第一个参数类型:

type FirstParameter<T extends FunctionType> = T extends (
  arg: infer P,
  ...args: any
) => any
  ? P
  : never;

type FuncFoo = (arg: number) => void;
type FuncBar = (...args: string[]) => void;

type FooFirstParameter = FirstParameter<FuncFoo>; // number

type BarFirstParameter = FirstParameter<FuncBar>; // string

(4)除了对函数类型进行模式匹配,内置工具类型中还有一组对 Class 进行模式匹配的工具类型:

type ClassType = abstract new (...args: any) => any;

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

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

 Class 的通用类型签名可能看起来比较奇怪,但实际上它就是声明了可实例化(new)与可抽象(abstract)罢了。我们也可以使用接口来进行声明:

export interface ClassType<TInstanceType = any> {
    new (...args: any[]): TInstanceType;
}

对 Class 的模式匹配思路类似于函数,或者说这是一个通用的思路,即基于放置位置的匹配。放在参数部分,那就是构造函数的参数类型,放在返回值部分,那当然就是 Class 的实例类型了。

(5)TypeScript 4.7 就支持了 infer 约束功能来实现对特定类型地提取。

type FirstArrayItemType<T extends any[]> = T extends [infer P extends string, ...any[]]
  ? P
  : never;

实际上,infer + 约束的场景是非常常见的,尤其是在某些连续嵌套的情况下,一层层的 infer 提取再筛选会严重地影响代码的可读性,而 infer 约束这一功能无疑带来了更简洁直观的类型编程代码。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值