看完还入不了 Ts 类型体操的门,打我

173 阅读6分钟

本文分为三个部分,第一部分自行推导一些 Ts 运算的辅助函数;第二部分实现 Ts 中的一些内置函数;最后一部分使用这些函数完成一些简单的任务。

本文需要一定的 Ts 基础,比如,你已经完全了解了 [] in infer extends keyof typeof 等这些操作符的作用。对这些内容有任何疑问,可以先参考 Typescript 官网说明

1. 传入 number 类型,返回此长度的数组

数组是我们进行 Ts 算数运算的基础,因此此泛型可以看成是基础的辅助函数。

实现原理:先给一个空数组,然后每次递归都给其添加一个元素,同时比较此时数组长度和传入的 number 类型是否先等,如果相等,输出当前数组,否则进行递归:

type Num2Array<
  Length extends number,
  Ele = unknown,
  Arr extends unknown[] = []
> = Arr["length"] extends Length ? Arr : Num2Array<Length, Ele, [...Arr, Ele]>;

Num2Array<3>; // [unknown, unknown, unknown]

风险点:传入的长度可能是负数。

2. 传入一个数组,去掉最后一个元素

我们可以通过扩展运算符给数组添加元素,而没有相应的做法给它减少一个元素。

实现原理:通过 extends + infer 的配合找出数组的最后一个元素并提出。

type DropLast<T extends any[]> = T extends [...infer Rest, infer Last] ? Rest : never;

3. 传入一个数组,在末尾增加一个元素

增加元素可以通过扩展运算符,这是被 Ts 本身承认的运算,但是要增加谁呢?

实现原理:利用泛型也可以有默认值的特点,给传入的数组类型添加一个 unknown 类型元素。

type PushNew<T extends any[], Ele = unknown> = T extends any[] ? [...T, Ele] : never;

4. 正整数加 1 减 1

在 Ts 中,number 类型和 Array 类型之间有千丝万缕的练习,我们在本文第一小节中定义 Num2Array 将 number 类型转成 Array 类型,那么这个过程可以反过来吗?

实现原理:将传入的 number 类型先转成 Array 类型,然后利用 PushNew 增加一个元素,之后再取当前数组的长度即可。

type Addone<T extends number> = PushNew<Num2Array<T>>['length'];

减 1 同理:

type Substractone<T extends number> = DropLast<Num2Array<T>>['length'];

风险点:传入的 number 可能是负数或者 0(对于减 1) 造成递归溢出。

5. 两正整数相加

Ts 中并不提供直接的类型之间的算数运算,因此想要实现加法,就必须间接使用数组的扩展运算。

实现原理:将两个加数转成对应长度的数组,然后通过扩展运算符将其拼接起来,最后取拼接数组的长度即可。

type Add<Num1 extends number, Num2 extends number> = [
  ...Num2Array<Num1>,
  ...Num2Array<Num2>
]["length"];

风险点:未对传入的 number 进行非负检测。

6. 两正整数相减

Ts 中的加法不是通过常规方法实现的,因此不能将减法简单视为加法的逆运算,实际上减法要复杂一些。

实现原理:先将被减数转成对应长度的数组,然后进行递归,每次递归时此数组长度减 1,计数器加 1,直到计数器值等于减数。计数器是通过另外一个数组的长度来表示的。

type Substract<T extends number, N extends number, Ele = unknown, K extends any[] = []> = {
  0: T,
  1: T extends number ? Substract<(DropLast<Num2Array<T>>)['length'], N, Ele, PushNew<K>> : never,
}[K extends { length: N } ? 0 : 1];

风险点:减数大于被减数导致递归溢出。

7. 判断 number 类型奇偶

在 Ts 中没有 %2 可以使用,我们只能通过递归的方法实现。

实现原理:不管是什么正整数,只要每次减 2,不停的减,最后不是 1 就是 0.

type IsOdd<N extends number> = {
  0: N extends 1 ? true : false,
  1: IsOdd<(DropLast<DropLast<Num2Array<N>>>)['length']>
}[
  N extends 0 | 1 ? 0 : 1
]

风险点:不支持传入负数,传入负数会导致递归溢出。

8. 判断是否为正整数

这可太难了,Ts 中虽然有 number 类型,但各个 number 之间本身没有高低之分,在数学层面上 2 > 1, 但是一旦认为这里的 2 1 为类型,就不存在这种比较。而所谓的正整数,其本质就是大于 0 的整数,既然比较是不存在的,那么判断其为正是非常困难的。

实现原理:这里巧妙的利用了 Ts 的递归栈溢出。在我的编辑器中,递归栈的最大深度为 999,因此我们找一个只对正整数生效的泛型,传入一个 number 类型,如果递归次数大于 999 则判定其为负数。

type LimmitedPositiveInteger<
  Length extends number,
  Ele = unknown,
  Arr extends unknown[] = []
> = Arr["length"] extends 999 ? false : Arr["length"] extends Length ? true : LimmitedPositiveInteger<Length, Ele, [...Arr, Ele]>;

风险点:只能判断 0 - 999 之间的正整数,假如传入 1000 也会返回 false, 因此上述方法并不完善,需改进;但是思路已经提供。可参考奇偶性实现方法。

实现正整数判定泛函之后,上面小节实现的一些泛函可以利用此优化一下。

9. 两正整数判断相等

这没什么好说的,在 js 中相等用 === 判断,在 ts 中,用关键字 extends.

type IsEqual<N extends number, M extends number> = N extends M ? true : false;

10. 两正整数判断大小

我们在第 6 小节已经实现了减法,仔细观察,如果传入的第一个 number 比第二个大,则返回 number 类型,否则返回 never 类型。

实现原理:首先排除两数相等,如果 N 小于 M 则 Substract 的结果为 never.

type GreaterThan<N extends number, M extends number> = N extends M ? false : (Substract<N, M> extends never ? false : true);
type LessThan<N extends number, M extends number> = N extends M ? false : (Substract<M, N> extends never ? false : true);

那么我们为什么不用 GreaterThan<N extends number, 0> 来代替正整数判断呢?

11. Ts 内置泛函实现

Ts 中提供了许多辅助函数,这和我们上面构造的十种类似,现在一一实现之。

11.1 Partial

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

11.2 Required

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

看看这个 -? 符号多可爱。

11.3 Readonly

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

11.4 Pick

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

11.5 Record

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

11.6 Exclude

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

11.7 Extract

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

11.8 Omit

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

11.9 NonNullable

type NonNullable<T> = T & {};

11.10 Parameters

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

11.11 ConstructorParameters

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

11.12 ReturnType

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

12.13 InstanceType

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

12. 实践部分

在这一小节中,我们使用上面的基础函数做一些简单的类型练习。

12.1 & 的练习

type ObjType = { a: number } & { c: boolean }
const oo: ObjType = {
  a: 1,
  // b: 1,
  c: true,
}

& 链接之后,不能多也不能少,见 11.9 NonNullable 的实现。

12.2 使用 UpperCase 泛函实现 Capitalize 泛函

type CapitalizeStr<Str extends string> =
    Str extends `${infer First}${infer Rest}`
    ? `${Uppercase<First>}${Rest}` : Str;

type CapitalizeResult = CapitalizeStr<'tang'>; // 'Tang'

12.3 将使用下划线的标识符命名转成驼峰命名

// a_b -> aB
type ToCanmel<T extends string> = T extends `${infer First}-${infer Second}${infer Rest}` ? `${First}${Uppercase<Second>}${Rest}` : never;
type DD = ToCanmel<'a-bandon'>; // "aBandon"

12.4 将一个类型数组反转

本质上还是递归。

type ReverseArr<Arr extends unknown[]> = Arr extends [...infer Rest, infer Last] ? [Last, ...ReverseArr<Rest>] : Arr;
type OriginArr = [1, 2, 3, 4];
type ReversedArr = ReverseArr<OriginArr>; // [4, 3, 2, 1]

13. 小结

  1. extends + infer 提取泛型 T 中更加细节的内容,当 T 满足 extends 后面的模式的时候,那么模式中对应的部分就应该可以被提取出来,这是显而易见的;但是却不好理解。
  2. Ts 中的递归是实现复杂类型的基本。

如果你现在充满了信心,可以来做一些真正的体操了:github.com/type-challe… 也可以在这里:www.cnblogs.com/laggage/cat…