TypeScript泛型数值运算工具类型实现
在TypeScript(TS)的类型系统中,工具类型是提升代码类型安全性和开发效率的重要工具。工具类型能在编译阶段完成数据校验、类型构造等工作,避免运行时的错误。下面将从一段实际的TS代码出发,解读如何用TS工具类型实现键盘按键组合类型定义和基础数值计算,分析TS类型系统的核心能力。
警告:本文涉及的数值计算相关工具类型,仅支持正整数运算,且受TypeScript编译器递归次数限制,处理过大数值时可能失效。
核心铺垫:构建数组工具类型BuildArray
解读复杂逻辑前,需先理解基础工具类型——BuildArray,这个类型是后续数值计算实现的核心基础。
//构建数组工具类型
type BuildArray<
L extends number, // 长度
E = any, // 数组元素类型
A extends any[] = [] // 数组(此处作为一个“变量”)
> = A['length'] extends L ? A : // 如果数组的长度到达了给定的长度 返回数组类型
BuildArray<L, E, [...A, E]>; // 否则递归循环构建,数组内的元素 + 1
这是一个递归泛型类型,核心作用是“根据指定长度L,构建一个长度为L的数组类型”。以下拆解其三个泛型参数和逻辑:
-
L extends number:目标数组的长度,约束为数值类型。 -
E = any:数组元素的类型,默认是any,可自定义。 -
A extends any[] = []:递归的"累加器",默认是空数组,用于逐步构建目标数组。
逻辑流程:判断累加器A的长度是否等于目标长度L。如果是,返回A;如果不是,递归调用自身,将E添加到A中(即[...A, E]),直到满足长度条件。
示例:BuildArray<3, string> 会生成 [string, string, string] 类型。该类型的核心价值在于:通过数组长度表示数值,将数值运算转化为数组操作,为类型层面的数值计算提供可能。
数值计算系列
基于BuildArray可实现类型层面的加减乘除运算,核心约束为:仅支持正整数运算,且受TypeScript编译器递归次数严格限制。这些运算均为编译阶段确定的常量计算,不影响运行时性能,但数值过大时会因递归超限而失效。
1. 加法Add:数组合并求长度
// 加法Add:数组合并求长度
type Add<A extends number, B extends number> =
A extends 0 ? B :
B extends 0 ? A :
[...BuildArray<A>, ...BuildArray<B>]['length'];
Add类型等价JS代码
// 对应TS Add类型的JS实现逻辑
// 思路:数组合并取长度模拟加法,与TS类型逻辑完全对齐
function Add(A, B) {
if (A === 0) return B; // 被加数为0返回加数
if (B === 0) return A; // 加数为0返回被加数
// 核心:构建对应长度数组并合并,取长度为结果(模拟TS的BuildArray)
return [...BuildArray(A), ...BuildArray(B)].length;
}
核心思路:两个正整数A和B相加,等价于"长度为A的数组"和"长度为B的数组"合并后的总长度。
优化点:增加了0的边界判断(如果A是0,直接返回B;如果B是0,直接返回A),避免不必要的递归。
验证:Add<2, 3> → 合并BuildArray<2>(长度2)和BuildArray<3>(长度3),总长度5 → 结果为5。
2. 减法Minus:数组拆分求剩余长度
type Minus<
A extends number,
B extends number
> = B extends 0 ? A :
A extends 0 ? 0 :
BuildArray<A> extends [...BuildArray<B>, ...infer R] ?
R["length"] : 0;
Minus类型等价JS代码
// 对应TS Minus类型的JS实现逻辑
// 思路:用数值比较替代TS的infer类型匹配,
function Minus(A, B) {
if (B === 0) return A; // 减数为0返回被减数
if (A === 0) return 0; // 被减数为0返回0
const remainder = A - B; // 计算差值,模拟TS的R["length"]
return remainder >= 0 ? remainder : 0; // 对应TS匹配失败返回0
}
核心思路:正整数A减正整数B,等价于"长度为A的数组"拆分成"长度为B的数组"和"剩余数组R",R的长度即为结果。
边界处理:如果B是0,返回A;如果A是0(或A<B,此时拆分失败),返回0。
验证:Minus<5, 2> → BuildArray<5> 可拆分为 BuildArray<2> + R,R长度为3 → 结果为3。
3. 乘法Multi:递归加法实现
// 乘法Multi:基于累加器的递归加法实现
// 泛型参数:
// A: 被乘数(正整数)
// B: 乘数(正整数)
// R: 累加器数组,存储每次累加的结果,默认空数组
type Multi<
A extends number,
B extends number,
R extends any[] = []
> = B extends 0 ? 0 : // 乘数为0时,结果为0(零乘任何数为0)
A extends 0 ? 0 : // 被乘数为0时,结果为0(任何数乘0为0)
B extends 1 ? [...R, ...BuildArray<A>]['length'] : // 乘数为1时,累加器合并A的数组后取长度
Multi<A, Minus<B, 1>, [...R, ...BuildArray<A>]>; // 乘数>1时,递归累加A并减少乘数
Multi类型等价JS代码
// 对应TS Multi类型的JS实现逻辑
// 思路:递归累加模拟乘法
function Multi(A, B, R = 0) { // R为数值累加器,对应TS的数组累加器
if (B === 0) return 0; // 乘数为0返回0
if (A === 0) return 0; // 被乘数为0返回0
const total = R + A; // 累加被乘数,模拟TS的数组合并
if (B === 1) return total; // 乘数为1返回累加结果
return Multi(A, B - 1, total); // 递归,乘数减1,传递累加结果
}
核心思路:正整数乘法通过“累加器数组”实现多次加法,将A重复累加B次,累加过程通过数组拼接存储中间结果,最终取数组长度作为结果。
逻辑流程:1. 若乘数B为0或被乘数A为0,直接返回0;2. 若B为1,将累加器数组R与长度为A的数组拼接,取拼接后数组长度作为结果;3. 若B>1,递归调用自身,乘数B减1,同时将长度为A的数组拼接到累加器R中,持续累加直到B变为1。
验证:Multi<3, 2> → 首次调用:B=2≠1,递归调用Multi<3, 1, BuildArray<3>> → 二次调用:B=1,拼接累加器(长度3)与BuildArray<3>(长度3),总长度6 → 结果为6。
4. 除法Div与取模Mod:递归减法实现
// 除法Div:基于递归减法的商计算(向下取整)
// 泛型参数:
// A: 被除数(正整数)
// B: 除数(正整数,不可为0)
// C: 商的累加器,记录减法执行次数,默认0
type Div<
A extends number,
B extends number,
C extends number = 0
> = B extends 0 ? never : // 除数为0时返回never(无意义运算)
A extends B ? Add<C, 1> : // 被除数等于除数时,商为累加器+1
Minus<A, B> extends 0 ? C : // 减后为0时,商为当前累加器
Div<Minus<A, B>, B, Add<C, 1>>; // 递归减去除数,商累加器+1
// 取模Mod:基于除法和乘法的余数计算
// 泛型参数:
// A: 被除数(正整数)
// B: 除数(正整数,不可为0)
type Mod<
A extends number,
B extends number,
> = B extends 0 ? never : Minus<A, Multi<Div<A, B>, B>>; // 余数=被除数-商×除数
Div和Mod类型等价JS代码
// 对应TS Div类型的JS实现逻辑
function Div(A, B, C = 0) { // C为商的累加器,对应TS的泛型C
if (B === 0) return void 0; // 除数为0返回无意义值(对应TS的never)
if (A === B) return C + 1; // 被除数等于除数,商+1
if (A - B <= 0) return C; // 减后为0,返回当前商
return Div(A - B, B, C + 1); // 递归减去除数,商累加器+1
}
// 对应TS Mod类型的JS实现逻辑
function Mod(A, B) {
if (B === 0) return void 0; // 除数为0返回无意义值
// 核心:余数=被除数-商×除数,与TS公式完全一致
return A - (Div(A, B) * B);
}
除法与取模实现依赖Add、Minus、Multi等基础运算类型,其中取模逻辑基于“余数=被除数-商×除数”的数学公式,先通过除法获取商,再通过乘法计算“商×除数”,最后通过减法得到余数。
-
除法Div:通过递归减去除数,用累加器C记录减法执行次数(即商)。当被除数小于除数时,累加器C即为结果;新增除数为0的防护,返回never表示无意义运算。
-
取模Mod:基于“余数=被除数-商×除数”的数学公式,先通过Div获取商,再通过Multi计算“商×除数”,最后通过Minus得到余数;同样添加除数为0的防护。
验证:1. Div<7, 3> → 7-3=4(C=1)→ 4-3=1(C=2)→ 1<3,返回2;2. Mod<7,3> → 先计算Div<7,3>=2,再计算Multi<2,3>=6,最后Minus<7,6>=1 → 结果为1;3. 若输入Div<5,0>,直接返回never。
5. 核心使用示例
以下提供数值运算工具类型的使用示例:
// 1. 常规正整数运算示例
type AddExample = Add<2, 3>; // 用法:Add<被加数, 加数>,推导为5
type MinusExample = Minus<5, 2>; // 用法:Minus<被减数, 减数>,推导为3
type MultiExample = Multi<3, 2>; // 用法:Multi<被乘数, 乘数>,推导为6
type DivExample = Div<7, 3>; // 用法:Div<被除数, 除数>,推导为2(向下取整)
type ModExample = Mod<7, 3>; // 用法:Mod<被除数, 除数>,推导为1
// 2. 关键边界场景示例
type AddWithZero = Add<5, 0>; // 含0加法,推导为5
type MinusWithZero = Minus<5, 0>; // 含0减法,推导为5
type MultiWithZero = Multi<5, 0>; // 含0乘法,推导为0
type MultiWithOne = Multi<5, 1>; // 含1乘法,推导为5
// 场景:定义固定数值相关的业务类型
type PageSize = Multi<10, 5>; // 页面大小,推导为50
type Remainder = Mod<100, 30>; // 计算余数,推导为10
const pageSize: PageSize = 50; // 合法:类型精准匹配
const remainder: Remainder = 10; // 合法:类型精准匹配