200字
TypeScript泛型数值运算工具类型实现
2025-11-30
2026-03-15

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;   // 合法:类型精准匹配

评论