本文分为三个部分,第一部分自行推导一些 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. 小结
- extends + infer 提取泛型 T 中更加细节的内容,当 T 满足 extends 后面的模式的时候,那么模式中对应的部分就应该可以被提取出来,这是显而易见的;但是却不好理解。
- Ts 中的递归是实现复杂类型的基本。
如果你现在充满了信心,可以来做一些真正的体操了:github.com/type-challe… 也可以在这里:www.cnblogs.com/laggage/cat…