# 进阶-计数-加减乘除

TS 的类型系统本身不支持数值运算 —— 1 + 1 在类型层不会得到 2。所有"类型数学"都要借助元组长度绕一圈。本文把加、减、乘、除、大小比较全部串起来。

# 核心工具:T['length']

元组类型的 length 属性,在类型层能直接拿到字面量 number:

type A = [any, any, any]['length']; // 3
type B = []['length']; // 0

这是把"元组形态"翻译回"数值形态"的唯一通路,所有算术题最终都要用到它。

# 构造指定长度的元组

反向操作:给一个 number N,造一个长度为 N 的元组。递归模板:

type BuildTuple<N extends number, R extends any[] = []> = R['length'] extends N
  ? R
  : BuildTuple<N, [...R, any]>;

type T = BuildTuple<5>; // [any, any, any, any, any]

原理:不断往 R 里 push,直到 R['length'] 等于 N。这是之后做加减乘除的"数值 → 元组"转换器。

# 加法

把两个元组拼起来,长度就是和:

type Add<A extends number, B extends number> = [
  ...BuildTuple<A>,
  ...BuildTuple<B>,
]['length'];

type R = Add<2, 3>; // 5

够用,但递归深度受限(一次 BuildTuple 每 1 递归一次)。

# 减法

减法是"在长的里面拆掉短的长度":

type Sub<A extends number, B extends number> = BuildTuple<A> extends [
  ...BuildTuple<B>,
  ...infer R,
]
  ? R['length']
  : never;

type R = Sub<5, 2>; // 3

如果 A < B,模式匹配失败返回 never;实际做题时可以把这个语义当成"下界为 0 的减法"。

# 比较大小

基于减法,就能做大小比较 —— 如果 A - B 成立则 A >= B

type GTE<A extends number, B extends number> = BuildTuple<A> extends [
  ...BuildTuple<B>,
  ...any,
]
  ? true
  : false;

type GT<A extends number, B extends number> = A extends B ? false : GTE<A, B>;

medium/4425-实现比较 就是这个套路。

另一种写法是"同时 +1 向前跑,看谁先到",在处理 hard/extreme 的整数比较器时会碰到,见 extreme/274-整数比较器

# 乘法

乘法是"加 B 次 A":

type Multi<
  A extends number,
  B extends number,
  R extends any[] = [],
> = B extends 0
  ? R['length']
  : Multi<A, Sub<B, 1> & number, [...R, ...BuildTuple<A>]>;

因为每乘一次要用 BuildTuple<A>,递归树会比较深,数值稍大就爆栈。实践中乘法题目(medium/extreme 都有)基本都是小数值。

一种更简洁但仍会爆栈的写法:

type Multi<
  A extends number,
  B extends number,
  R extends any[] = [],
> = R['length'] extends B
  ? Flatten<R>['length'] // R 是 [A, A, ..., A] B 个,展开后长度就是 A*B
  : Multi<A, B, [...R, BuildTuple<A>]>;

# 除法

除法是"看 A 能减几次 B":

type Div<
  A extends number,
  B extends number,
  C extends any[] = [],
> = BuildTuple<A> extends [...BuildTuple<B>, ...infer R]
  ? Div<R['length'] & number, B, [...C, 1]>
  : C['length'];

type R = Div<10, 3>; // 3

同样只处理整数,余数靠减法算。

# 递归深度红线

上面所有操作本质都是 [...A, B] 的元组递归。TS 对递归有硬限制:

  • TS 4.5 之前:尾递归 50 层
  • TS 4.5+:1000 层(启用 tail-recursion 优化后)。

所以:

  • 数值不能太大;
  • BuildTuple 要写成尾递归形式(累加器在参数位置);
  • 计数时每递归一次只 push 1 个元素,不要写成指数级。

更多讨论见 递归深度

# 计数器模式的通用性

元组长度计数不只用来算"数学",更常用的场景是给递归"编号":

// 数一个字符串里有多少个 'a'
type CountA<
  S extends string,
  C extends any[] = [],
> = S extends `${infer F}${infer R}`
  ? F extends 'a'
    ? CountA<R, [...C, 1]>
    : CountA<R, C>
  : C['length'];

type N = CountA<'banana'>; // 3

凡是要"在递归里记某件事发生了几次""记当前是第几项"的场合,辅助元组都派得上用场。

# 字符串 ↔ 数字

有时题目给你字符串形式的数字(比如 "123"),要在类型层做数学,需要先把它变回数字:

type ToNum<S extends string> = S extends `${infer N extends number}`
  ? N
  : never;
type A = ToNum<'42'>; // 42

反过来数字转字符串直接模板:

type N = 42;
type S = `${N}`; // '42'

带运算的字符串数字题(如 medium/10969-整数)都会混合使用这些操作。

# 速查表

运算 模板骨架
N → tuple BuildTuple<N>:递归 push 到 length === N
tuple → N T['length']
A + B [...BuildTuple<A>, ...BuildTuple<B>]['length']
A - B BuildTuple<A> extends [...BuildTuple<B>, ...infer R] ? R['length'] : never
A * B 递归:每轮把 BuildTuple<A> 拼进累加器
A / B 递归:每轮从 A 里削掉一个 BuildTuple<B>,计数
A >= B BuildTuple<A> extends [...BuildTuple<B>, ...any] ? true : false

# 被谁用到

看到"类型层的数字运算"反射这一套模板就行,剩下就是控好递归深度。

Last Updated: 2026/4/19 00:26:40