08-套路四:数组长度做计数
课程
1
如何阅读本小册
已学完
学习时长: 2分6秒
2
为什么说 TypeScript 的火爆是必然?
已学完
学习时长: 4分28秒
3
TypeScript 类型编程为什么被叫做类型体操?
已学完
学习时长: 9分16秒
4
TypeScript 类型系统支持哪些类型和类型运算?
已学完
学习时长: 19分27秒
5
套路一:模式匹配做提取
已学完
学习时长: 41分32秒
6
套路二:重新构造做变换
已学完
学习时长: 39分51秒
7
套路三:递归复用做循环
学习时长: 42分22秒
8
套路四:数组长度做计数
学习时长: 27分58秒
9
套路五:联合分散可简化
学习时长: 19分36秒
10
套路六:特殊特性要记清
学习时长: 36分41秒
11
类型体操顺口溜
学习时长: 27分26秒
12
TypeScript 内置的高级类型有哪些?
学习时长: 31分59秒
13
真实案例说明类型编程的意义
学习时长: 34分36秒
14
类型编程综合实战一
学习时长: 25分55秒
15
类型编程综合实战二
学习时长: 29分9秒
16
新语法 infer extends 是如何简化类型编程的
学习时长: 11分14秒
17
原理篇:逆变、协变、双向协变、不变
学习时长: 11分31秒
18
原理篇:编译 ts 代码用 tsc 还是 babel?
学习时长: 19分49秒
19
原理篇:实现简易 TypeScript 类型检查
学习时长: 47分51秒
20
原理篇:如何阅读 TypeScript 源码
学习时长: 19分27秒
21
原理篇:一些特殊情况的说明
已学完
学习时长: 12分39秒
22
小册总结
学习时长: 2分32秒
23
加餐:3 种类型来源和 3 种模块语法
学习时长: 17分6秒
24
加餐:用 Project Reference 优化 tsc 编译性能
已学完
学习时长: 4分47秒
juejin_logo copyCreated with Sketch.

TypeScript 类型系统不是图灵完备,各种逻辑都能写么,但好像没发现数值相关的逻辑。

没错,数值相关的逻辑比较绕,被我单独摘了出来,就是这节要讲的内容。

这是类型体操的第四个套路:数组长度做计数。

数组长度做计数

TypeScript 类型系统没有加减乘除运算符,怎么做数值运算呢?

不知道大家有没有注意到数组类型取 length 就是数值。

比如:

而数组类型我们是能构造出来的,那么通过构造不同长度的数组然后取 length,不就是数值的运算么?

TypeScript 类型系统中没有加减乘除运算符,但是可以通过构造不同的数组然后取 length 的方式来完成数值计算,把数值的加减乘除转化为对数组的提取和构造。

(严格来说构造的是元组,大家知道数组和元组的区别就行)

这点可以说是类型体操中最麻烦的一个点,需要思维做一些转换,绕过这个弯来。

下面我们就来做一些真实的案例来掌握它吧。

数组长度实现加减乘除

Add

我们知道了数值计算要转换为对数组类型的操作,那么加法的实现很容易想到:

构造两个数组,然后合并成一个,取 length。

比如 3 + 2,就是构造一个长度为 3 的数组类型,再构造一个长度为 2 的数组类型,然后合并成一个数组,取 length。

构造多长的数组是不确定的,需要递归构造,这个我们实现过:

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

类型参数 Length 是要构造的数组的长度。类型参数 Ele 是数组元素,默认为 unknown。类型参数 Arr 为构造出的数组,默认是 []。

如果 Arr 的长度到达了 Length,就返回构造出的 Arr,否则继续递归构造。

构造数组实现了,那么基于它就能实现加法:

type Add<Num1 extends number, Num2 extends number> = 
    [...BuildArray<Num1>,...BuildArray<Num2>]['length'];

我们拿大一点的数测试下:

结果是对的。

试一下

就这样,我们通过构造一定长度的数组取 length 的方式实现了加法运算。

Subtract

加法是构造数组,那减法怎么做呢?

减法是从数值中去掉一部分,很容易想到可以通过数组类型的提取来做。

比如 3 是 [unknown, unknown, unknown] 的数组类型,提取出 2 个元素之后,剩下的数组再取 length 就是 1。

所以减法的实现是这样的:

type Subtract<Num1 extends number, Num2 extends number> = 
    BuildArray<Num1> extends [...arr1: BuildArray<Num2>, ...arr2: infer Rest]
        ? Rest['length']
        : never;

类型参数 Num1、Num2 分别是被减数和减数,通过 extends 约束为 number。

构造 Num1 长度的数组,通过模式匹配提取出 Num2 长度个元素,剩下的放到 infer 声明的局部变量 Rest 里。

取 Rest 的长度返回,就是减法的结果。

试一下

就这样,我们通过数组类型的提取实现了减法运算。

有同学可能会问,后面那部分需要 infer 提取,所以起个 arr2 的名字没问题。前面那部分不需要名字呀,可以去掉 arr1 么?

试一下就知道了:

报错显示元组成员或者全部有名字,或者全部没有。

Multiply

我们把加法转换为了数组构造,把减法转换为了数组提取。那乘法怎么做呢?

为了解释乘法,我去翻了下小学教材,找到了这样一张图:

1 乘以 5 就相当于 1 + 1 + 1 + 1 + 1,也就是说乘法就是多个加法结果的累加。

那么我们在加法的基础上,多加一个参数来传递中间结果的数组,算完之后再取一次 length 就能实现乘法:

type Mutiply<
    Num1 extends number,
    Num2 extends number,
    ResultArr extends unknown[] = []
> = Num2 extends 0 ? ResultArr['length']
        : Mutiply<Num1, Subtract<Num2, 1>, [...BuildArray<Num1>, ...ResultArr]>;

类型参数 Num1 和 Num2 分别是被加数和加数。

因为乘法是多个加法结果的累加,我们加了一个类型参数 ResultArr 来保存中间结果,默认值是 [],相当于从 0 开始加。

每加一次就把 Num2 减一,直到 Num2 为 0,就代表加完了。

加的过程就是往 ResultArr 数组中放 Num1 个元素。

这样递归的进行累加,也就是递归的往 ResultArr 中放元素。

最后取 ResultArr 的 length 就是乘法的结果。

试一下

就这样,我们通过递归的累加实现了乘法。

Divide

乘法是递归的累加,那除法不就是递归的累减么?

我再去翻了下小学教材,找到了这样一张图:

我们有 9 个苹果,分给美羊羊 3 个,分给懒羊羊 3 个,分给沸羊羊 3 个,最后剩下 0 个。所以 9 / 3 = 3。

所以,除法的实现就是被减数不断减去减数,直到减为 0,记录减了几次就是结果。

也就是这样的:

type Divide<
    Num1 extends number,
    Num2 extends number,
    CountArr extends unknown[] = []
> = Num1 extends 0 ? CountArr['length']
        : Divide<Subtract<Num1, Num2>, Num2, [unknown, ...CountArr]>;

类型参数 Num1 和 Num2 分别是被减数和减数。

类型参数 CountArr 是用来记录减了几次的累加数组。

如果 Num1 减到了 0 ,那么这时候减了几次就是除法结果,也就是 CountArr['length']。

否则继续递归的减,让 Num1 减去 Num2,并且 CountArr 多加一个元素代表又减了一次。

这样就实现了除法:

试一下

就这样,我们通过递归的累减并记录减了几次实现了除法。

做完了加减乘除,我们再来做一些别的数值计算的类型体操。

数组长度实现计数

StrLen

数组长度可以取 length 来得到,但是字符串类型不能取 length,所以我们来实现一个求字符串长度的高级类型。

字符串长度不确定,明显要用递归。每次取一个并计数,直到取完,就是字符串长度。

type StrLen<
    Str extends string,
    CountArr extends unknown[] = []
> = Str extends `${string}${infer Rest}` 
    ? StrLen<Rest, [...CountArr, unknown]> 
    : CountArr['length']

类型参数 Str 是待处理的字符串。类型参数 CountArr 是做计数的数组,默认值 [] 代表从 0 开始。

每次通过模式匹配提取去掉一个字符之后的剩余字符串,并且往计数数组里多放入一个元素。递归进行取字符和计数。

如果模式匹配不满足,代表计数结束,返回计数数组的长度 CountArr['length']。

这样就能求出字符串长度:

试一下

GreaterThan

能够做计数了,那也就能做两个数值的比较。

我们往一个数组类型中不断放入元素取长度,如果先到了 A,那就是 B 大,否则是 A 大:

type GreaterThan<
    Num1 extends number,
    Num2 extends number,
    CountArr extends unknown[] = []
> = Num1 extends Num2 
    ? false
    : CountArr['length'] extends Num2
        ? true
        : CountArr['length'] extends Num1
            ? false
            : GreaterThan<Num1, Num2, [...CountArr, unknown]>;

类型参数 Num1 和 Num2 是待比较的两个数。

类型参数 CountArr 是计数用的,会不断累加,默认值是 [] 代表从 0 开始。

如果 Num1 extends Num2 成立,代表相等,直接返回 false。

否则判断计数数组的长度,如果先到了 Num2,那么就是 Num1 大,返回 true。

反之,如果先到了 Num1,那么就是 Num2 大,返回 false。

如果都没到就往计数数组 CountArr 中放入一个元素,继续递归。

这样就实现了数值比较。

当 3 和 4 比较时:

当 6 和 4 比较时:

试一下

Fibonacci

谈到了数值运算,就不得不提起经典的 Fibonacci 数列的计算。

Fibonacci 数列是 1、1、2、3、5、8、13、21、34、…… 这样的数列,有当前的数是前两个数的和的规律。

F(0) = 1,F(1) = 1, F(n) = F(n - 1) + F(n - 2)(n ≥ 2,n ∈ N*)

也就是递归的加法,在 TypeScript 类型编程里用构造数组来实现这种加法:

type FibonacciLoop<
    PrevArr extends unknown[], 
    CurrentArr extends unknown[], 
    IndexArr extends unknown[] = [], 
    Num extends number = 1
> = IndexArr['length'] extends Num
    ? CurrentArr['length']
    : FibonacciLoop<CurrentArr, [...PrevArr, ...CurrentArr], [...IndexArr, unknown], Num> 

type Fibonacci<Num extends number> = FibonacciLoop<[1], [], [], Num>;

类型参数 PrevArr 是代表之前的累加值的数组。类型参数 CurrentArr 是代表当前数值的数组。

类型参数 IndexArr 用于记录 index,每次递归加一,默认值是 [],代表从 0 开始。

类型参数 Num 代表求数列的第几个数。

判断当前 index 也就是 IndexArr['length'] 是否到了 Num,到了就返回当前的数值 CurrentArr['length']。

否则求出当前 index 对应的数值,用之前的数加上当前的数 [...PrevArr, ... CurrentArr]。

然后继续递归,index + 1,也就是 [...IndexArr, unknown]。

这就是递归计算 Fibinacci 数列的数的过程。

可以正确的算出第 8 个数是 21:

试一下

总结

TypeScript 类型系统没有加减乘除运算符,所以我们通过数组类型的构造和提取,然后取长度的方式来实现数值运算

我们通过构造和提取数组类型实现了加减乘除,也实现了各种计数逻辑。

用数组长度做计数这一点是 TypeScript 类型体操中最麻烦的一个点,也是最容易让新手困惑的一个点。

本文案例的合并

留言
Ctrl + Enter
全部评论(60)
慢功夫的头像
删除
前端工程师
type multiple2<Num1 extends number, Num2 extends number, Result extends number = 0> = Num2 extends 0
? Result
: multiple2<Num1, subTract<Num2, 1>,Add<Num1, Result>>;

光哥,这个用Add的地方,为啥报这个错误
展开
点赞
3
删除
(作者)
这里计算不出结果,你加个 & number 就行了,相当于断言一定是 number
1
回复
删除
Ok
这里计算不出结果,你加个 & number 就行了,相当于断言一定是 number
点赞
回复
查看更多回复
Asuka14024的头像
删除
前端工程师
利用GreaterThan稍微改进了下除法
```TypeScript
type Subtract<Num1 extends number, Num2 extends number> = BuildArray<Num1> extends [...BuildArray<Num2>, ...infer Rest] ? Rest['length'] : never;

type GreaterThan<Num1 extends number, Num2 extends number, Arr extends unknown[] = []> = Arr['length'] extends Num1
? false : Arr['length'] extends Num2 ? true : GreaterThan<Num1, Num2, [unknown, ...Arr]>;

type Divide<Num1 extends number, Num2 extends number, Result extends unknown[] = []> =
GreaterThan<Num1, Num2> extends true ? Divide<Subtract<Num1, Num2>, Num2, [unknown, ...Result]> : Num1 extends Num2 ? [unknown, ...Result]['length'] : Result['length'];
```
展开
点赞
1
删除
优化不可除尽的场景
点赞
回复
deer的头像
删除
前端开发
不要案例中的名字,减法可以这样写么?
type Substract<
num1 extends number,
num2 extends number
> = BuildArr<num1> extends [...BuildArr<num2>, ...infer Rest]
? [...Rest]['length']
: never;
展开
点赞
1
删除
(作者)
写法不唯一
点赞
回复
踏雪1024的头像
删除
F(0) = 1,F(1) = 1, F(n) = F(n - 1) + F(n - 2)(n ≥ 2,n ∈ N*)
如果索引从 0 开始算起
type Fibonacci<Num extends number> = FibonacciLoop<[1], [], [], Num>;
要改成
type Fibonacci<Num extends number> = FibonacciLoop<[], [1], [], Num>
1
1
删除
似乎是的,当前值为1
点赞
回复
Dxing的头像
删除
关于 infer 的用法有官方文档可以参考吗
点赞
回复
hedgehog_boy的头像
删除
示例除法只能是Num1整除Num2的,6/5是返回的是never
2
回复
hanxiaoxin的头像
删除
IT美食家 @ 天朝上国
将Add类型拿出去复用的时候 type AA<T extends number = 0> = () => AA<Add<T, 1>> 经常会提示:类型“Add<T, 1>”不满足约束“number”的情况。
type Add<Num1 extends number, Num2 extends number> = Num1 extends number ? [...BuildArray<Num1>, ...BuildArray<Num2>]['length'] : never
上面一句将Add类型通过never计算一次就能过。这个我估计跟上一章讲的,typescript类型在未被使用的时候不会计算,所以需要never强行计算一遍。
这个未使用应该是因为AA本身也是基于泛型的动态类型。你写成确定的type AA<T extends number = 0> = () => AA<Add<5, 1>>就没这个问题了
展开
点赞
1
删除
不太明白
点赞
回复
CV专家的头像
删除
专业CV选手
配图让我感觉我在上幼儿园[捂脸]
点赞
2
删除
(作者)
[看]
点赞
回复
删除
你个黑粉
点赞
回复
brownz的头像
删除
web前端开发
type Add<Num1 extends number, Num2 extends number> = [...BuildArray<Num1>,...BuildArray<Num2>]['length'];

type AA<T extends number = 0> = () => AA<Add<T, 1>>;

为什么这里会不满足constraint?
Type 'Add<T, 1>' does not satisfy the constraint 'number'.(2344)
展开
点赞
回复
用户3409995695192的头像
删除
type StrLen<Str extends string, Length extends number = 0> = Str extends `${string}${infer Rest}` ? StrLen<Rest, Add<Length, 1>> : 0;

为什么计算字符串长度时如果使用Add<Length, 1>去累加每个字符的长度,会报错:【类型“Add<Length, 1>”不满足约束“number”】?如果不考虑功能的话,Add换成减乘除都是不会报错的,只有Add比较特殊。同时如果直接写成Add<2, 1>的话也是不会报错的,是Length这个泛型有什么问题吗
展开
点赞
1
删除
+1
点赞
回复
焦糖色的橙子的头像
删除
其他的例子都 ok Fibonacci 数列给我干蒙蔽了 看不懂
点赞
3
删除
function acc(pre, current, index = 0, num) {
if (index === num) {
return current;
}
if (index < num) {
return acc(current, pre + current, index + 1, num);
}
}
// 要求:
acc(preValue, preCurrent, 0, 1);
acc(preValue, preCurrent, 0, 2);
问preValue 、 preCurrent 的值为多少?

//解:
根据上面的函数逻辑,得出两个公式:
preValue + preCurrent = 1 ;
preValue + 2*preCurrent = 1 ;

//求得:
用第二个等式 减去 第一个等式 得:
preValue + 2*preCurrent - preValue + preCurrent = 1-1
等式经过计算后得:
preCurrent = 0 ;
由于 preCurrent 为 0 ,
由上面等式 得出
preValue=1 ;

//测试:
acc(1, 0, 0, 1);//1
acc(1, 0, 0, 2);//1
acc(1, 0, 0, 3);//2
acc(1, 0, 0, 4);//3
acc(1, 0, 0, 5);//5
展开
点赞
回复
删除
Fibonacci 数列最难理解的是,如何设计preValue, preCurrent 的值,这里演算的就是如何求出preValue, preCurrent 值
function acc(pre, current, index = 0, num) {
if (index === num) {
return current;
}
if (index < num) {
return acc(current, pre + current, index + 1, num);
}
}
// 要求:
acc(preValue, preCurrent, 0, 1);
acc(preValue, preCurrent, 0, 2);
问preValue 、 preCurrent 的值为多少?

//解:
根据上面的函数逻辑,得出两个公式:
preValue + preCurrent = 1 ;
preValue + 2*preCurrent = 1 ;

//求得:
用第二个等式 减去 第一个等式 得:
preValue + 2*preCurrent - preValue + preCurrent = 1-1
等式经过计算后得:
preCurrent = 0 ;
由于 preCurrent 为 0 ,
由上面等式 得出
preValue=1 ;

//测试:
acc(1, 0, 0, 1);//1
acc(1, 0, 0, 2);//1
acc(1, 0, 0, 3);//2
acc(1, 0, 0, 4);//3
acc(1, 0, 0, 5);//5
点赞
回复
查看更多回复
Asuka14024的头像
删除
前端工程师
总感觉这个技巧用处不算大
点赞
1
删除
(作者)
涉及到计数的逻辑就需要用这个了
2
回复
带带大菜鸡的头像
删除
乞讨工程师
打卡
点赞
回复
国立的头像
删除
递归似乎不能太深。比如,那个乘法,我就遇到type r1 = Mutiply<30, 100>; 是可以的。
但是type r2 = Mutiply<30, 1000>; 就不行了,
报错是 “类型实例化过深,且可能无限。ts(2589)”
2
1
删除
(作者)
是的,递归层数有限制
点赞
回复
IsLand的头像
删除
结果是喜羊羊6个,懒羊羊3个。因为沸羊羊会把自己的苹果给美羊羊,美羊羊把自己的苹果给喜羊羊[不失礼貌的微笑]
12
4
删除
(作者)
[看]
点赞
回复
删除
哈哈哈哈
点赞
回复
查看更多回复
wu0792wu的头像
删除
涉及负数就没法搞了
2
回复
爱吃鱼的桶哥Z的头像
删除
伪 · 全栈打杂攻城狮
这一章的内容确实是有些绕啊,需要多看几遍,不过越学越有意思了[不失礼貌的微笑]
1
1
删除
(作者)
[碰拳]
点赞
回复
abc_xxx的头像
删除
野生前端
光哥 , ts中负数字符串类型可以转成负数数字类型吗
点赞
1
删除
(作者)
不可以,只能取 length
点赞
回复
小迷的头像
删除
前端
数字的加减这部分觉得需要仔细看看,觉得这部分我有点儿疑惑
点赞
回复
瓦嘞嘞的头像
删除
type Fib<Num extends number> = Add<
Fib<Subtract<Num, 1>>,
Fib<Subtract<Num, 2>>
>
光哥为啥这样写数列不行啊
点赞
1
删除
Fib 没终止条件?
点赞
回复

查看全部 60 条回复