本文通过在 typescript 中实现斐波那契类型约束,初窥 typescript 的高级应用;文章是我实现过程中的心路历程,希望对你有所启发。文章结尾总结了一些使用 typescript 的感悟,不知道我们想法是否相同。
首先必须明确,在 TS 中,我们讨论的运算是以类型作为基础的,也就是说,参与运算的必须是类型,如果你不是类型,那将降级到 Javascript 的范畴中,这不是我们要讨论的对象。
1. 尝试阶段
在这个阶段,我对 typescript 中的类型运算进行了基本的探究,如下代码所示:
type a = [1]; // [1]
type b = [1]; // [1]
type c = [...a, ...b]; // [1, 1]
type p = c['length']; // 2
type x = c[0]; // 1
type q = c[c['length']-1] // 报错
总结一下,Javascript 中的值经过 type 作用之后才能变成类型,才具有在 Ts 环境中运算的先决条件,通过上面的代码,我们不难看出:
[1]是可以作为类型的;[...a, ...b]扩展运算在 Ts 中是允许的;c['length']我们仍然可以通过 length 属性获取数组的长度;c[0]我们可以通过下标获取数组的元素;c[c['length']-1]这种写法是非法的,因为c['length']-1是非法的,尽管c['length']得到 2,但是此 2 非 彼 2,这个 2 表示 【类型 2】 不可以和数值 1 做算数运算。
2. Ts 中的递归运算
在 Ts 中做递归的基本形式为:
{
0: 出口;
1: 递归项目;
}[T extends 递归进行下去的条件 ? 1 : 0];
类似于,
type example<T extends any[], K extends number = 20> = {
0: T;
1: example<[...T, 1]>;
}[T extends {length: K} ? 1 : 0];
看起来挺复杂的,实际上一点也不难,原理非常简单:
- 前半部分是
{0: --, 1: --}这就是一个以 number 类型作为 key 的 object 字面量; - 后半部分是
[0] 或者 [1]这明显就是在取前面 object 中对应键的值,当递归条件被满足的时候值就是 1,否则就是 0;
3. 去掉数组的最后一个元素
我们使用上面的模式,完成去掉类型数组最后一个类型的操作,之所以这么做是因为 Ts 中不支持 c[c['length']-1] 这样的操作,因此为了得到最后一个元素的下标,我们只能将 c 数组最后一个元素去掉成为 d 数组,这样 d['length'] 就可以表示最后一个元素的下标了;同理,去掉 d 的最后一个元素得到 e 数组,由 e['length'] 就可以得到 c 数组倒数第二个元素的下标了。
这个办法看起来很笨,但是受限于 Ts 的运算限制,目前只能先这样。
type DropLast<T extends any[]> = {
0: T;
1: ((...args: T) => void) extends ((head: any, ...tail: infer U) => void) ? U : never;
}[T extends [...any[], any] ? 1 : 0];
或者,
type DropLast<T extends any[]> = T extends [...infer Rest, infer Last] ? Rest : never;
4. 加法的实现
在 Typescript 中如果我们想要将【类型 5】和【类型 8】相加得到【类型 13】,是一件非常困难的事情,我们实现其的原理为:根据【类型 5】构造出一个长度为 5 的数组,根据【类型 8】构造出一个长度为 8 的数组,然后用扩展运算符合并这两个数组,取其长度即可。
type BuildArray<
Length extends number,
Ele = unknown,
Arr extends unknown[] = []
> = Arr['length'] extends Length
? Arr
: BuildArray<Length, Ele, [...Arr, Ele]>;
type Add<Num1 extends number, Num2 extends number> =
[...BuildArray<Num1>, ...BuildArray<Num2>]['length'];
BuildArray 的逻辑为,刚开始的时候 Arr 是空数组类型 [] 然后比较其 length 属性和传入的 length 类型(这里不能用 === 而是用 extends,因为对于类型,不能说 2 === 2,只能说 2 extends 2);如果条件不成立,则递归调用,并改变 Arr 的类型,通过扩展运算符使其长度增加 1,直到满足条件再返回当前数组的长度。
5. 探寻规律
现在我们有了辅助函数 DropLast 和 Add,就使用这两个函数来将斐波那契数列写下去:
// type x = c[DropLast<c>['length']];
// type y = c[DropLast<DropLast<c>>['length']];
// type z = Add<x,y>;
// type d = [...c, z];
type d = [...c, Add<c[DropLast<c>['length']], c[DropLast<DropLast<c>>['length']]>]; // [1, 1, 2]
// type x2 = d[DropLast<d>['length']]
// type y2 = d[DropLast<DropLast<d>>['length']]
// type z2 = Add<x2, y2>
// type e = [...d, z2];
type e = [...d, Add<d[DropLast<d>['length']], d[DropLast<DropLast<d>>['length']]>]; // [1, 1, 2, 3]
type f = [...e, Add<e[DropLast<e>['length']], e[DropLast<DropLast<e>>['length']]>]; // [1, 1, 2, 3, 5]
我们很容易总结出规律为:
type next<T extends number[]> = Add<T[DropLast<T>['length']], T[DropLast<DropLast<T>>['length']]>;
// 得到一般性规律
type g = [...f, next<f>]; // [1, 1, 2, 3, 5, 8]
6. 将规律写成类型
给变量换个名,罗列在一起观察:
type n1 = [1]; // [1]
type n2 = [1]; // [1]
type n3 = [...n1, ...n2]; // [1, 1]
type n4 = [...n3, Add<n3[DropLast<n3>['length']], n3[DropLast<DropLast<n3>>['length']]>]; // [1, 1, 2]
type n5 = [...n4, Add<n4[DropLast<n4>['length']], n4[DropLast<DropLast<n4>>['length']]>]; // [1, 1, 2, 3]
type n6 = [...n5, Add<n5[DropLast<n5>['length']], n5[DropLast<DropLast<n5>>['length']]>]; // [1, 1, 2, 3, 5]
type n7 = [...n6, next<n6>]; // [1, 1, 2, 3, 5, 8]
利用递归模板写出一般性的规律:
type Fibonacci<T extends any[], Next extends number = 0> = {
0: T;
1: Fibonacci<[...T, next<T>], Next>;
}[T extends { length: Next } ? 0 : 1];
然后进行测试:
type f5 = Fibonacci<[1, 1], 5>; // [1, 1, 2, 3, 5]
type f15 = Fibonacci<[1, 1], 15>; // [1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610]
7. 优化泛型参数顺序
将上面的斐波那契类型写成更加符合直觉的形式:
type FibonacciClassic<Next extends number, T extends any[] = [1, 1]> = {
0: T;
1: FibonacciClassic<Next, [...T, next<T>]>;
}[T extends { length: Next } ? 0 : 1];
type fc5 = FibonacciClassic<5>; // [1, 1, 2, 3, 5]
8. 进一步拓展
更多情况下,我们可能并不想得到整个数组,我们只想要数组的最后一个值,或者说其联合类型,为此,对上面的代码进行拓展:
type tuple2union<T extends any[], Acc = never> = T extends []
? never // 基础情况:空元组转换为 never 联合类型
: T extends [infer First, ...infer Rest]
? First | tuple2union<Rest, Acc>
: never;
// 测试用例
type Test1 = tuple2union<FibonacciClassic<3>>; // 应输出 1 | 2
type Test2 = tuple2union<FibonacciClassic<7>>; // 应输出 1 | 2 | 3 | 5 | 8 | 13
type LastElement<T extends any[]> = T extends [...infer Rest, infer Last] ? Last : never;
5
// @ts-ignore
type FibonacciAt<T extends number> = LastElement<FibonacciClassic<T>>; // 有超出最大递归深度的风险
// 测试用例
type Test3 = FibonacciAt<5>; // 应输出 5
type Test4 = FibonacciAt<9>; // 应输出 34
9. 全部代码
下面的代码展示了我们实现斐波那契数列类型的全部代码,并对其进行了一些优化:
type HelperArray<
Length extends number,
Ele = unknown,
Arr extends unknown[] = []
> = Arr['length'] extends Length
? Arr
: BuildArray<Length, Ele, [...Arr, Ele]>;
type HelperAdd<Num1 extends number, Num2 extends number> =
[...HelperArray<Num1>, ...HelperArray<Num2>]['length'];
type NextEle<T extends number[]> = T extends [...infer Rest, infer Last2, infer Last] ? HelperAdd<Last2, Last> : never;
type Fibo<Next extends number, T extends number[] = [1, 1]> = {
0: T;
1: Fibo<[...T, NextEle<T>], Next>;
}[T extends { length: Next } ? 0 : 1];
type Fibo7 = FibonacciClassic<7>; // [1, 1, 2, 3, 5, 8, 13]
type example<T extends any[], K extends number = 20> = {
0: T;
1: example<[...T, 1]>;
}[T extends { length: K } ? 1 : 0];
10. 总结
相信本文还是有一定的难度的,这里就不总结技巧性的内容了,只总结普适的思想:
- 由于 Ts 中开放了通过数组的 length 属性得到数字类型的口子,所以我们可以通过数组来间接实现整数运算
- 加法:演化成两个数组拼接取长度。
- 加 1 或者减 1:演化成先构造数组,然后增加一个元素或者减少一个元素取长度。
- 下面这个递归模板很好用,特别是在【构造】一些基础结构的时候
{
0: 出口;
1: 递归项目;
}[T extends 递归进行下去的条件 ? 1 : 0];
- 当我们需要从类型数组中提取一些内容的时候(例如取最后一个元素,或者最后两个元素)使用 infer
type DropLast<T extends any[]> = T extends [...infer Rest, infer Last] ? Rest : never;
- 类型不能用 == 或者 === , 如果你想要表达同样的含义使用 extends, 例如上面的
2 extends 2
并且,结合三元表达式,威力巨大:
T extends { length: K } ? 1 : 0
这里多说一句:
- 如果想要判断数组类型的长度是否为 10,写成:
T extends { length: 10 } ? 1 : 0
好了,就到此为止了,希望你收获满满!