Typescript进阶之类型体操套路四

套路四:数组长度做计数

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

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

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

数组长度做计数

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

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

比如:

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

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

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

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

数组长度实现加减乘除

Add

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

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

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

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

typescript 复制代码
type BuildArray<
    Length extends number, 
    Ele = unknown, 
    Arr extends unknown[] = []
> = Arr['length'] extends Length 
        ? Arr 
        : BuildArray<Length, Ele, [...Arr, Ele]>;
​

解释

  1. 泛型参数 Length extends number, Ele = unknown, Arr extends unknown[] = []

    • Length 是一个泛型参数,表示目标数组的长度,必须是一个数字。
    • Ele 是一个可选的泛型参数,默认值为 unknown,表示数组中每个元素的类型。
    • Arr 是一个可选的泛型参数,默认值为 [],表示当前构建的数组。
  2. 条件类型 Arr['length'] extends Length

    • 检查当前构建的数组 Arr 的长度是否等于目标长度 Length
    • Arr['length'] 获取数组 Arr 的长度。
  3. 递归处理 BuildArray<Length, Ele, [...Arr, Ele]>

    • 如果当前数组的长度还没有达到目标长度,则递归调用 BuildArray,并向当前数组 Arr 中添加一个新元素 Ele
    • 新数组的长度会增加 1。
  4. 默认情况 ? Arr

    • 如果当前数组的长度已经等于目标长度,则返回当前数组 Arr

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

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

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

scala 复制代码
type Add<Num1 extends number, Num2 extends number> = 
    [...BuildArray<Num1>,...BuildArray<Num2>]['length'];
​
go 复制代码
// 示例 1: 计算 2 + 3
type Sum1 = Add<2, 3>;
// 结果:5

// 示例 2: 计算 0 + 0
type Sum2 = Add<0, 0>;
// 结果:0
ini 复制代码
// 示例 1: 构建长度为 3 的数组,元素类型为 `unknown`
type Array1 = BuildArray<3>;
// 结果:[unknown, unknown, unknown]

// 示例 2: 构建长度为 3 的数组,元素类型为 `string`
type Array2 = BuildArray<3, string>;
// 结果:[string, string, string]

Subtract

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

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

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

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

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

解释

  1. 泛型参数 Num1 extends number, Num2 extends number

    • Num1Num2 是泛型参数,表示传入的类型必须是数字。
  2. 使用 BuildArray 创建数组

    • BuildArray<Num1> 创建一个长度为 Num1 的数组。
    • BuildArray<Num2> 创建一个长度为 Num2 的数组。
  3. 模式匹配 BuildArray<Num1> extends [...arr1: BuildArray<Num2>, ...arr2: infer Rest]

    • 使用模式匹配来检查 BuildArray<Num1> 是否可以表示为 [...arr1: BuildArray<Num2>, ...arr2: infer Rest]
    • arr1 是长度为 Num2 的数组。
    • Rest 是剩余的数组部分。
  4. 获取剩余数组的长度

    • 如果模式匹配成功,则 Rest 是从 BuildArray<Num1> 中移除 BuildArray<Num2> 后的剩余数组。
    • Rest['length'] 获取剩余数组的长度,这个长度就是 Num1 减去 Num2 的结果。
  5. 默认情况 : never

    • 如果模式匹配失败(即 Num1 小于 Num2),则返回 never 类型。

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

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

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

go 复制代码
// 示例 1: 计算 5 - 2
type Diff1 = Subtract<5, 2>;
// 结果:3

// 示例 2: 计算 3 - 3
type Diff2 = Subtract<3, 3>;
// 结果:0
  • Num15Num22
  • BuildArray<5> 创建一个长度为 5 的数组 [unknown, unknown, unknown, unknown, unknown]
  • BuildArray<2> 创建一个长度为 2 的数组 [unknown, unknown]
  • 模式匹配成功,剩余数组是 [unknown, unknown, unknown],长度为 3

Multiply

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

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

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

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

typescript 复制代码
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 就是乘法的结果。

解释

  1. 泛型参数 Num1 extends number, Num2 extends number, ResultArr extends unknown[] = []

    • Num1Num2 是泛型参数,表示传入的类型必须是数字。
    • ResultArr 是一个可选的泛型参数,默认值为 [],表示当前的结果数组。
  2. 条件类型 Num2 extends 0

    • 检查 Num2 是否为 0。
    • 如果 Num2 为 0,则返回结果数组的长度 ResultArr['length'],这就是最终的乘积。
  3. 递归处理 Multiply<Num1, Subtract<Num2, 1>, [...BuildArray<Num1>, ...ResultArr]>

    • 如果 Num2 不为 0,则递归调用 Multiply,将 Num2 减 1。
    • Num1 对应的数组 BuildArray<Num1> 添加到当前的结果数组 ResultArr 中。
  4. 使用 Subtract 类型别名

    • Subtract<Num2, 1> 用于计算 Num2 - 1
go 复制代码
// 示例 1: 计算 3 * 2
type Product1 = Multiply<3, 2>;
// 结果:6
  • Num13Num22
  • 初始 ResultArr[]
  • 第一次递归:Num22,不为 0,调用 Multiply<3, 1, [unknown, unknown, unknown]>
  • 第二次递归:Num21,不为 0,调用 Multiply<3, 0, [unknown, unknown, unknown, unknown, unknown, unknown]>
  • 第三次递归:Num20,返回 ResultArr 的长度 6

Divide

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

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

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

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

也就是这样的:

typescript 复制代码
type Divide<
    Num1 extends number,
    Num2 extends number,
    CountArr extends unknown[] = []
> = Num1 extends 0 ? CountArr['length']
        : Divide<Subtract<Num1, Num2>, Num2, [unknown, ...CountArr]>;
​

解释

  1. 泛型参数 Num1 extends number, Num2 extends number, CountArr extends unknown[] = []

    • Num1Num2 是泛型参数,表示传入的类型必须是数字。
    • CountArr 是一个可选的泛型参数,默认值为 [],表示当前的计数器数组。
  2. 条件类型 Num1 extends 0

    • 检查 Num1 是否为 0。
    • 如果 Num1 为 0,则返回计数器数组的长度 CountArr['length'],这就是最终的商。
  3. 递归处理 Divide<Subtract<Num1, Num2>, Num2, [unknown, ...CountArr]>

    • 如果 Num1 不为 0,则递归调用 Divide,将 Num1 减去 Num2
    • unknown 添加到当前的计数器数组 CountArr 中。
  4. 使用 Subtract 类型别名

    • Subtract<Num1, Num2> 用于计算 Num1 - Num2

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

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

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

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

这样就实现了除法:

go 复制代码
// 示例 1: 计算 6 / 2
type Quotient1 = Divide<6, 2>;
// 结果:3

// 示例 2: 计算 8 / 3
type Quotient2 = Divide<8, 3>;
// 结果:2
  • Num16Num22
  • 初始 CountArr[]
  • 第一次递归:Num16,不为 0,调用 Divide<4, 2, [unknown]>
  • 第二次递归:Num14,不为 0,调用 Divide<2, 2, [unknown, unknown]>
  • 第三次递归:Num12,不为 0,调用 Divide<0, 2, [unknown, unknown, unknown]>
  • 第四次递归:Num10,返回 CountArr 的长度 3

数组长度实现计数

StrLen

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

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

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

解释

  1. 泛型参数 Str extends string, CountArr extends unknown[] = []

    • Str 是一个泛型参数,表示传入的类型必须是字符串。
    • CountArr 是一个可选的泛型参数,默认值为 [],表示当前的计数器数组。
  2. 条件类型 Str extends {string}${infer Rest}`

    • 检查 Str 是否可以表示为 ${string}${infer Rest},其中 Rest 是剩余的字符串部分。
    • infer Rest 用于推断剩余的字符串部分。
  3. 递归处理 StrLen<Rest, [...CountArr, unknown]>

    • 如果 Str 不为空,则递归调用 StrLen,将剩余的字符串 Rest 传递给下一次递归。
    • unknown 添加到当前的计数器数组 CountArr 中。
  4. 默认情况 : CountArr['length']

    • 如果 Str 为空,则返回计数器数组的长度 CountArr['length'],这就是最终的字符串长度。

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

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

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

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

go 复制代码
// 示例 1: 计算 "abc" 的长度
type Len2 = StrLen<'abc'>;
// 结果:3
  • Str"abc"
  • 初始 CountArr[]
  • 第一次递归:Str"abc",不为空,调用 StrLen<'bc', [unknown]>
  • 第二次递归:Str"bc",不为空,调用 StrLen<'c', [unknown, unknown]>
  • 第三次递归:Str"c",不为空,调用 StrLen<'', [unknown, unknown, unknown]>
  • 第四次递归:Str'',返回 CountArr 的长度 3

总结

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

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

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

相关推荐
web150850966414 小时前
【React&前端】大屏适配解决方案&从框架结构到实现(超详细)(附代码)
前端·react.js·前端框架
理想不理想v4 小时前
前端项目性能优化(详细)
前端·性能优化
CodeToGym4 小时前
使用 Vite 和 Redux Toolkit 创建 React 项目
前端·javascript·react.js·redux
Cachel wood5 小时前
Vue.js前端框架教程8:Vue消息提示ElMessage和ElMessageBox
linux·前端·javascript·vue.js·前端框架·ecmascript
PP东6 小时前
ES6学习Generator 函数(生成器)(八)
javascript·学习·es6
桃园码工7 小时前
4_使用 HTML5 Canvas API (3) --[HTML5 API 学习之旅]
前端·html5·canvas
桃园码工7 小时前
9_HTML5 SVG (5) --[HTML5 API 学习之旅]
前端·html5·svg
人才程序员7 小时前
QML z轴(z-order)前后层级
c语言·前端·c++·qt·软件工程·用户界面·界面
m0_548514777 小时前
前端三大主流框架:React、Vue、Angular
前端·vue.js·react.js
m0_748232397 小时前
单页面应用 (SPA):现代 Web 开发的全新视角
前端