☰
type-challenges 极端难度 Subtract:用 BuildTuple 实现类型级减法
2026/10/2 13:46:09 网站建设 项目流程
  • 示例工程

【免费下载链接】type-challenges

Collection of TypeScript type challenges with online judge

项目地址:https://gitcode.com/GitHub_Trending/ty/type-challenges
点击查看免费下载

导读

本篇文章围绕 type-challenges 仓库中编号 07561 的极端(extreme)难度题目Subtract展开,讲解如何仅靠 TypeScript 类型系统、借助 "用元组长度映射数字"(BuildTuple)这一经典手段实现类型层面的整数减法,并正确处理"被减数小于减数时返回never"的边界规则。读完本文你将掌握基于元组(tuple)长度做类型级算术的核心套路、读懂该题目配套模板与测试用例的约束,并理解这类"简单版"实现为什么在数字变大时会触碰 TypeScript 递归实例化深度限制。

一、题目概览:一道专注"简单减法"的 extreme 题目

Subtract 是 type-challenges 中的一道extreme(极端)难度题目,其元数据(见 info.yml)标注:

  • 难度:extreme
  • 标签:tuple(元组)
  • 作者:Lo(GitHub: @LoTwT)

题目出自 README.md,核心要求只有一句话:

Implement the type Subtraction that is-in Javascript by using BuildTuple.

即在类型层面实现 JavaScript 的减法语义(-运算符),且解题路线被明确指定为BuildTuple——先构造元组,再通过元组长度完成数值运算。题目同时强调了两点约束:

  1. If the minuend is less than the subtrahend, it should benever.当被减数小于减数(即结果为负数)时,返回never而非负数;
  2. It's a simple version.题目自称为"简单版",暗示该实现并不追求覆盖任意大整数,这在配套测试用例中也有体现(详见下文第四节)。

二、需求规格拆解:输入、输出与边界

题目给出的示例非常简洁:

Subtract<2, 1> // expect to be 1 Subtract<1, 2> // expect to be never

结合模板与测试,可以将行为规格完整归纳为一张表:

表达式结果含义
Subtract<M, S>且 M > SM - S正常减法
Subtract<M, S>且 M === S0相等时结果为 0
Subtract<M, S>且 M < Snever结果为负,直接否决

其中两个类型参数的命名在模板中已有注释(见 template.ts):

// M => minuend, S => subtrahend type Subtract<M extends number, S extends number> = any
  • M即minuend(被减数);
  • S即subtrahend(减数);
  • 两个参数均被约束为number,模板默认实现为any,等待解题者替换。

之所以要求"小于时返回never",是因为类型系统没有"负数"这一原生表示,用元组长度无法直接表达负数结果,因此用never显式拒绝该输入组合,避免产生错误推断。

三、测试驱动:从 test-cases.ts 读懂验收标准

配套测试用例位于 test-cases.ts,全文如下:

import type { Equal, Expect } from '@type-challenges/utils' type cases = [ Expect<Equal<Subtract<1, 1>, 0>>, Expect<Equal<Subtract<2, 1>, 1>>, Expect<Equal<Subtract<1, 2>, never>>, // @ts-expect-error Expect<Equal<Subtract<1000, 999>, 1>>, ]

逐行解读验收标准:

  1. Subtract<1, 1>必须精确等于0:相等输入的处理是必测项,说明实现不能只覆盖"大减小";
  2. Subtract<2, 1>必须精确等于1:最基础的正常减法;
  3. Subtract<1, 2>必须精确等于never:验证"被减数小于减数"时返回never的规则;
  4. @ts-expect-error标注的Subtract<1000, 999>:这是"简单版"最重要的证据。@ts-expect-error的含义是"下一行预期会产生类型错误"——由于1000规模的元组递归构造会触碰 TypeScript 的实例化深度限制,这行求值会报错,因此被测试作者用该指令"豁免",避免Expect断言误伤整个用例集。

测试中使用的Equal/Expect工具类型来自仓库的 utils/index.d.ts:

export type Expect<T extends true> = T export type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false

Expect要求传入的必须是字面量true;Equal采用"函数签名比较"的经典技巧,能够区分1与number这类宽窄类型,从而保证减法结果必须是精确的字面量类型而非number。

四、核心原理:用 BuildTuple 把数字"翻译"成元组长度

题目指定使用 BuildTuple 解法。其核心思想是类型系统的两个已知事实:

  • 元组的length属性是字面量数字类型,例如[unknown, unknown]['length']精确等于2;
  • 通过递归拼接元组,可以把任意数字N映射为长度恰为N的元组,从而让类型系统"数数"。

仓库中同作者(同为 @LoTwT)的另一道 medium 题目 Construct Tuple(07544) 正是这一能力的直接练习题,其要求为:

type result = ConstructTuple<2> // expect to be [unknown, unknown]

Subtract 与 Construct Tuple 一脉相承:减法可以转换为"从长度为 M 的元组中移除 S 个元素,剩下元组的长度即为M - S"。

BuildTuple 的递归构造

一个标准的 BuildTuple 实现如下(供理解参考,非仓库模板内容):

type BuildTuple<L extends number, T extends unknown[] = []> = T['length'] extends L ? T : BuildTuple<L, [...T, unknown]>

执行过程可以这样理解:

  1. 从空元组[]开始;
  2. 每次递归追加一个unknown元素,即[...T, unknown];
  3. 当T['length']与目标L相等时终止递归并返回T。

例如BuildTuple<2>会依次经历[]→[unknown]→[unknown, unknown],最终得到长度为2的元组。

用模式匹配做"减法"

有了 BuildTuple,减法可以通过元组解构(variadic tuple types)完成:先分别构造BuildTuple<M>与BuildTuple<S>,再用[...BuildTuple<S>, ...infer R]去匹配长元组,剩余部分R的长度就是差值:

type BuildTuple<L extends number, T extends unknown[] = []> = T['length'] extends L ? T : BuildTuple<L, [...T, unknown]> type Subtract<M extends number, S extends number> = M extends S ? 0 : BuildTuple<M> extends [...BuildTuple<S>, ...infer R] ? R['length'] : never

逻辑分支说明:

  • M extends S ? 0:先处理相等情形,Subtract<1, 1>直接命中0,避免走元组构造;
  • BuildTuple<M> extends [...BuildTuple<S>, ...infer R]:尝试从长元组前段"剥掉" S 个元素;
    • 若匹配成功,说明M >= S,剩余R['length']即为差值,如Subtract<2, 1>→[unknown]的length为1;
    • 若匹配失败(M < S),说明减数元组比被减数元组还长,永远无法匹配,落入never分支,如Subtract<1, 2>→never。

五、"简单版"的边界:递归深度限制的证据

题目 README 强调 "It's a simple version",这一声明在测试用例中得到印证:Expect<Equal<Subtract<1000, 999>, 1>>被@ts-expect-error包裹。

从源码结构可以推断:BuildTuple 采用逐元素递归构造,Subtract<1000, 999>需要先递归生成长度 1000 的元组,而 TypeScript 对类型实例化深度有硬性限制。当递归层级超过该限制时,编译器会直接报 "Type instantiation is excessively deep and possibly infinite" 一类的错误,Equal断言自然无法成立。测试作者因此用@ts-expect-error将其显式豁免——既保留了"大数场景下求值会报错"这一事实的可见性,又不会让整个用例集失败。

这带来的实际约束是:

  • 本实现适合M、S在几十以内的小整数场景;
  • 若要支持1000及以上规模的数字,需要切换到"按位/字符串逐位计算"等更复杂的方案,例如仓库中 Sum(00476)、Integers Comparator(00274) 等 extreme 题目所采用的 template-literal + 逐位进位策略。

六、如何本地验证你的实现

type-challenges 的题目采用"类型检查即测试"的模式,无需运行 JavaScript:

  1. 将你的实现写入 template.ts 中的Subtract类型(替换默认的any);
  2. 使用 TypeScript 编译器对 test-cases.ts 做类型检查(如tsc --noEmit);
  3. 若所有Expect<Equal<...>>均成立,说明实现通过验收;若某个Equal不成立,Expect<T extends true>会给出类型错误提示。

测试文件依赖@type-challenges/utils提供的Equal/Expect(定义见 utils/index.d.ts),这也是 type-challenges 全部题目共用的验收工具集。

七、延伸阅读:仓库中的"元组与数字"题目家族

Subtract 并非孤立存在,围绕"用元组/字符串做类型级算术",仓库中还有一系列可对照学习的题目:

题目难度技术要点
Construct Tuple(07544)medium用元组长度映射数字,Subtract 的 BuildTuple 前置技能
MinusOne(02257)medium数字减一,同样依赖元组长度递减
Sum(00476)extreme支持大数与 bigint 的加法,采用逐位计算规避递归深度限制
Integers Comparator(00274)extreme支持负数与零的整数比较器

对比可见:以元组长度为"算盘"的简单方案(Subtract、MinusOne)实现直观、易于理解,但受递归深度限制;而以字符串逐位运算的方案(Sum、Integers Comparator)复杂度更高,却能突破数字规模的上限。理解 Subtract,正是踏入 type-challenges 类型级算术世界的第一步。

  • 示例工程

【免费下载链接】type-challenges

Collection of TypeScript type challenges with online judge

项目地址:https://gitcode.com/GitHub_Trending/ty/type-challenges
点击查看免费下载
上一篇:Call Summary: [Company] — [Date]
下一篇:A2UI v0.8 自定义 Catalog 协商机制详解:从一次性能力声明到按消息、按 Surface 的动态目录选择

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询