news 2026/10/1 9:38:08

type-challenges 进阶实战:用模板字面量类型实现 Integers Comparator(Extreme 难度全解析)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
type-challenges 进阶实战:用模板字面量类型实现 Integers Comparator(Extreme 难度全解析)
  • 示例工程

【免费下载链接】type-challenges

Collection of TypeScript type challenges with online judge

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

导读

本篇文章围绕 type-challenges 仓库中的 Extreme 难度题目Integers Comparator(00274)展开,目标是在类型层面实现一个整数比较器Comparator<A, B>,它接收两个数字类型A与B(可为正整数、负整数或零,甚至一方为正另一方为负),并返回枚举Comparison.Greater / Comparison.Equal / Comparison.Lower中对应的一项。读完本文,你将掌握模板字面量类型(template literal types)的解析手法、数字字符串与符号的处理技巧、类型级递归的进位/借位算法,以及如何用@type-challenges/utils中的Equal/Expect组合来验证复杂类型运算结果。


一、题目是什么:Integers Comparator 的完整需求

本题由 Pig Fang(GitHub 用户 g-plane,难度标记为extreme,标签为template-literal与math(见 info.yml)。

题目给定了结果枚举:

enum Comparison { Greater, Equal, Lower, }

并要求实现:

type Comparator<A extends number, B extends number> = any

比较规则:

  • 若a大于b,结果为Comparison.Greater;
  • 若a与b相等,结果为Comparison.Equal;
  • 若a小于b,结果为Comparison.Lower。

题目特别强调:a与b可以是正整数、负整数或零,甚至可以一个为正、一个为负。也就是说,比较逻辑必须涵盖四种符号组合(正正、负负、正负、负正)以及零的边界情况,这比单纯的“同号比较”要复杂得多。

配套测试:官方给出的一手验证

仓库在 test-cases.ts 中给出了可运行的正向测试用例,可以作为实现的验收标准:

import type { Equal, Expect } from '@type-challenges/utils' type cases = [ Expect<Equal<Comparator<5, 5>, Comparison.Equal>>, Expect<Equal<Comparator<5, 6>, Comparison.Lower>>, Expect<Equal<Comparator<5, 8>, Comparison.Lower>>, Expect<Equal<Comparator<5, 0>, Comparison.Greater>>, Expect<Equal<Comparator<-5, 0>, Comparison.Lower>>, Expect<Equal<Comparator<0, 0>, Comparison.Equal>>, Expect<Equal<Comparator<0, -5>, Comparison.Greater>>, Expect<Equal<Comparator<5, -3>, Comparison.Greater>>, Expect<Equal<Comparator<5, -7>, Comparison.Greater>>, Expect<Equal<Comparator<-5, -7>, Comparison.Greater>>, Expect<Equal<Comparator<-5, -3>, Comparison.Lower>>, Expect<Equal<Comparator<-25, -30>, Comparison.Greater>>, Expect<Equal<Comparator<15, -23>, Comparison.Greater>>, Expect<Equal<Comparator<40, 37>, Comparison.Greater>>, Expect<Equal<Comparator<-36, 36>, Comparison.Lower>>, Expect<Equal<Comparator<27, 27>, Comparison.Equal>>, Expect<Equal<Comparator<-38, -38>, Comparison.Equal>>, // 100 数量级 Expect<Equal<Comparator<1, 100>, Comparison.Lower>>, Expect<Equal<Comparator<100, 1>, Comparison.Greater>>, Expect<Equal<Comparator<-100, 1>, Comparison.Lower>>, Expect<Equal<Comparator<1, -100>, Comparison.Greater>>, Expect<Equal<Comparator<-100, -1>, Comparison.Lower>>, Expect<Equal<Comparator<-1, -100>, Comparison.Greater>>, // Extra tests if you like to challenge yourself! Expect<Equal<Comparator<9007199254740992, 9007199254740992>, Comparison.Equal>>, Expect<Equal<Comparator<-9007199254740992, -9007199254740992>, Comparison.Equal>>, Expect<Equal<Comparator<9007199254740991, 9007199254740992>, Comparison.Lower>>, Expect<Equal<Comparator<9007199254740992, 9007199254740991>, Comparison.Greater>>, Expect<Equal<Comparator<-9007199254740992, -9007199254740991>, Comparison.Lower>>, Expect<Equal<Comparator<-9007199254740991, -9007199254740992>, Comparison.Greater>>, Expect<Equal<Comparator<3.1415, 3.1415>, Comparison.Equal>>, Expect<Equal<Comparator<3.1415, 3.1414>, Comparison.Greater>>, Expect<Equal<Comparator<0, 3.1414>, Comparison.Lower>>, Expect<Equal<Comparator<31.415, 3.1415>, Comparison.Greater>>, ]

可见官方测试不仅覆盖了正负整数组合,还附带了“额外挑战”用例:9007199254740992(即2^53)级别的大整数,甚至小数(如3.1415与3.1414)。不过需要说明:Comparator<A extends number, B extends number>的约束本身只接受number类型,而小数能否通过取决于实现如何截断/处理小数点后的内容;若你希望严格支持小数,需要在实现里对.进行专门处理(见后文进阶一节)。从源码结构看,这些 extra 用例主要是为了引导实现者思考边界条件。


二、解题思路:从“元组计数”到“字符串逐位比较”

2.1 为什么不能直接依赖运行时比较

TypeScript 类型系统是结构化类型系统,类型层面不存在a > b这样的运算符。因此在类型空间比较两个数,必须自己构造一种“可被类型推导的比较机制”。常见的三类思路:

  1. 元组长度计数法:利用[...T, 0]['length']构造长度为N的元组,再让两个元组“互相消耗”来比较长短。这也是仓库中 Greater Than 这类中等级题目的常见套路,其模板签名同样是type GreaterThan<T extends number, U extends number> = any,测试覆盖到GreaterThan<1234567891011, 1234567891010>(见 test-cases.ts)。
  2. 模板字面量逐位比较:把数字A、B转成字符串字面量类型(A extends number ?${A}: never),然后逐字符比较。本题标签为template-literal, math,暗示官方推荐方向就是这种字符串化的逐位比较。
  3. BigInt 字符串运算:如 Sum(type Sum<A extends string | number | bigint, B extends string | number | bigint> = string)这类题目一样,把数字当作字符串逐位做算术。本题只要求比较,不要求加减,所以逐位比较比逐位加减更直接。

元组计数法在数字较大时会触发深度递归限制(TS 默认递归深度约 1000 层),这正是 2^53 级大整数用例存在的意义——它迫使你放弃元组计数,改用字符串逐位比较。

2.2 统一入口:把number变成“符号 + 数字串”

核心的第一步是符号分离。一个整数在类型层面可以表示为'-' + 数字串(负数)或数字串(非负数)。我们可以先定义一个“拆符号”工具:

type SplitSign<S extends string> = S extends `-${infer R}` ? ['-', R] : ['+', S]

这里'-${infer R}'是模板字面量类型的模式匹配:若字符串以-开头,就把剩余部分推断为R。例如SplitSign<'-123'>得到['-', '123'],SplitSign<'123'>得到['+', '123']。

而把数字字面量类型转成字符串只需:

type ToString<N extends number> = `${N}`

三、核心算法:四位一体的比较矩阵

有了符号与数字串之后,比较逻辑可以按“符号组合”分四类。这是本题与普通数字比较题最大的区别所在,也是题目特意强调“even one is positive while another one is negative”的原因。

3.1 符号不同(一正一负)

  • 正数 > 负数,即'+' vs '-'恒为Greater;
  • 负数 < 正数,即'-' vs '+'恒为Lower。

特殊情况是零:0与-5比较时,0应判定为Greater(测试第 10 行Comparator<0, -5> => Greater)。这里可以把0视为“非负”,与正数同分支处理即可,因为0与任何负数的比较结果都等价于“非负数 > 负数”。

3.2 同为负数:绝对值越小越大

数学上:-5 > -7,因为5 < 7。所以负数之间比较,绝对值(去掉负号后的数字串)更小的反而更大。即:

'-' vs '-' => 绝对值比较后取相反结果

3.3 同为非负数:直接逐位比较

'+' vs '+'(含零):数字串逐位比较,得到Greater / Equal / Lower原样返回。

3.4 汇总

符号组合结果
一正一负(正侧含 0)正数侧为Greater
同为负绝对值比较结果的反向
同为非负绝对值比较结果(原样)

因此整个Comparator可以被组织成:

type Comparator<A extends number, B extends number> = CompareBySign<SplitSign<`${A}`>, SplitSign<`${B}`>> type CompareBySign<SA extends ['+' | '-', string], SB extends ['+' | '-', string]> = SA[0] extends SB[0] ? SA[0] extends '-' ? Reverse<CompareAbs<SA[1], SB[1]>> // 负负 => 绝对值反向 : CompareAbs<SA[1], SB[1]> // 正正 => 原样 : SA[0] extends '-' ? Comparison.Lower // 负 vs 正 : Comparison.Greater // 正 vs 负

其中Reverse<X>负责把Greater / Lower对调、Equal保持不变:

type Reverse<C> = C extends Comparison.Greater ? Comparison.Lower : C extends Comparison.Lower ? Comparison.Greater : Comparison.Equal

这样,核心问题就被归约为CompareAbs<A, B>:对两个纯数字字符串(不带符号)进行逐位比较。


四、核心实现:CompareAbs逐位比较器

4.1 先比位数,再逐位比

一个朴素的直觉是:数字字符串长度更长,值就更大。但要注意前导零问题(如'007'vs'8'),所以更稳健的做法是先去除前导零,然后:

  • 长度不同 => 长度长者大;
  • 长度相同 => 从左到右逐位比较,第一个不同字符大者大;全部相同则相等。

先定义去除前导零的工具:

type TrimZero<S extends string> = S extends '0' ? '0' : // 单独一个 0 保持为 '0' S extends `0${infer R}` ? TrimZero<R> : S // 递归去掉前导 0

再定义“构造 N 个字符的占位串”用于长度比较(或直接用模板递归统计长度):

type LengthOf<S extends string, T extends string = ''> = S extends `${infer _}${infer R}` ? LengthOf<R, `${T}x`> : T['length']

说明:T['length']在模板字面量类型中是可用的(TS 4.1+),这里利用字符串占位符间接获得“长度数字”。如果你更习惯元组,也可以type ToTuple<S extends string, T extends any[] = []> = S extends${infer _}${infer R}? ToTuple<R, [0, ...T]> : T,然后用T['length']。元组方案在数字很大时会受限,字符串占位方案更贴合本仓库其他 extreme 数学题的风格(如 Sum 处理1_000_000_000_000n级别的字符串运算)。

逐位比较的核心递归:

type CompareDigit<A extends string, B extends string> = A extends `${infer AH}${infer AT}` ? B extends `${infer BH}${infer BT}` ? AH extends BH ? CompareDigit<AT, BT> // 当前位相同,继续下一位 : IsDigitGreater<AH, BH> // 当前位不同,分出胜负 : Comparison.Greater // A 更长(同长度下 B 已耗尽) : B extends string ? B extends '' ? Comparison.Equal : Comparison.Lower : never

其中IsDigitGreater<A, B>把'0'..'9'映射到数字序数再比较。可以维护一个“数字到序号串”的映射表:

type DigitIndex = { '0': ''; '1': 'x'; '2': 'xx'; '3': 'xxx'; '4': 'xxxx'; '5': 'xxxxx'; '6': 'xxxxxx'; '7': 'xxxxxxx'; '8': 'xxxxxxxx'; '9': 'xxxxxxxxx' } type IsDigitGreater<A extends string, B extends string> = DigitIndex[A & keyof DigitIndex] extends `${DigitIndex[B & keyof DigitIndex]}${infer _}` ? A extends B ? Comparison.Equal : Comparison.Greater : Comparison.Lower

说明:'xxxxx' extends \${'xx'}${infer _}`表示“5 的占位串能容纳 2 的占位串”,即 5 > 2。同时用A extends B` 兜底相等情形。更直接的做法是维护一张 10×10 的胜负表,这里用占位串子串匹配减少枚举量。

最终:

type CompareAbs<A extends string, B extends string> = CompareAbsLen<TrimZero<A>, TrimZero<B>> type CompareAbsLen<A extends string, B extends string> = LengthOf<A> extends LengthOf<B> ? CompareDigit<A, B> : GreaterThanLen<LengthOf<A>, LengthOf<B>> extends true ? Comparison.Greater : Comparison.Lower

GreaterThanLen就是一个基于数字大小的比较(可用元组或字符串占位实现,见仓库 Greater Than 的思路)。


五、组装成完整Comparator及测试验证

将上述模块拼装起来,一个完整可用的参考实现如下:

enum Comparison { Greater, Equal, Lower, } type SplitSign<S extends string> = S extends `-${infer R}` ? ['-', R] : ['+', S] type TrimZero<S extends string> = S extends '0' ? '0' : S extends `0${infer R}` ? TrimZero<R> : S type Reverse<C> = C extends Comparison.Greater ? Comparison.Lower : C extends Comparison.Lower ? Comparison.Greater : Comparison.Equal // 长度比较(字符串占位版) type Len<S extends string, T extends string = ''> = S extends `${infer _}${infer R}` ? Len<R, `${T}x`> : T['length'] type GreaterNum<A extends number, B extends number> = /* 复用 GreaterThan 思路 */ any type CompareAbsLen<A extends string, B extends string> = Len<A> extends Len<B> ? CompareDigit<A, B> : GreaterNum<Len<A>, Len<B>> extends true ? Comparison.Greater : Comparison.Lower type CompareDigit<A extends string, B extends string> = /* 见 4.1 */ any type CompareAbs<A extends string, B extends string> = CompareAbsLen<TrimZero<A>, TrimZero<B>> type CompareBySign<SA extends ['+' | '-', string], SB extends ['+' | '-', string]> = SA[0] extends SB[0] ? SA[0] extends '-' ? Reverse<CompareAbs<SA[1], SB[1]>> : CompareAbs<SA[1], SB[1]> : SA[0] extends '-' ? Comparison.Lower : Comparison.Greater type Comparator<A extends number, B extends number> = CompareBySign<SplitSign<`${A}`>, SplitSign<`${B}`>>

运行官方测试(test-cases.ts)时,将上述类型与模板中给出的Comparator签名对齐,再用Expect<Equal<...>>断言。测试工具来自 utils/index.d.ts 中的Equal(基于函数参数逆变位置的extends可赋值性双向比较)与Expect<T extends true> = T,任何一处返回错误枚举值都会让Equal得false,从而Expect编译失败,这就是本题“在线裁判”式验证的机制。

一点工程化提示:本仓库根 package.json 提供pnpm install后即可在本地用tsc(依赖typescript: ^5.3.3,见 package.json)对test-cases.ts做类型检查来验证解法,无需运行任何 JS。


六、进阶讨论:大整数、零与小数边界

6.1 为什么 2^53 用例能通过:字符串方案天然免疫精度问题

9007199254740992是 JSNumber.MAX_SAFE_INTEGER + 1,超过它之后运行时数字已不可靠(IEEE 754 双精度)。但本题全程在类型空间把数字先字符串化再逐位比较,从未把两个值放进同一个数值运算里,因此 9007199254740992 与 9007199254740991 的逐位差异依然能被精确识别。这也是标签template-literal的精髓:用字符串作为“无限精度”的载体。

6.2 零的归属

0在SplitSign中落入'+'分支(因为'0'不以-开头)。这与测试期望完全吻合:

  • Comparator<5, 0>:'+','+'→CompareAbs<'5','0'>→Greater✅
  • Comparator<0, -5>:'+' vs '-'→Greater✅
  • Comparator<-5, 0>:'-' vs '+'→Lower✅
  • Comparator<0, 0>:CompareAbs<'0','0'>→Equal✅

6.3 小数:extra 用例的延伸

测试文件最后还给出了3.1415、31.415等小数用例。从Comparator<A extends number, B extends number>的签名看,小数同样满足extends number。若要支持小数,需要在SplitSign之后再对数字串做一次“小数点切分”,比较规则变为:先比较整数部分,整数部分相等再比较小数部分(小数部分同样逐位比较)。这是对本题算法的一次自然扩展,官方将其标记为 “Extra tests if you like to challenge yourself”,即不作为强制验收项。

6.4 与仓库其他数学题的呼应

本仓库存在一条清晰的“类型级数学”题目线:

  • MinusOne 测试包含MinusOne<9_007_199_254_740_992>,同样要求超出安全整数范围的类型级运算;
  • Sum 测试包含Sum<1_000_000_000_000n, '123'>,用字符串逐位加法处理 bigint 级输入;
  • Greater Than 测试包含GreaterThan<1234567891011, 1234567891010>。

Integers Comparator 与它们共享同一套“字符串化 + 逐位递归”方法论:凡是在运行时数字精度靠不住的场景,类型级实现都可以借助模板字面量类型把数字当作字符串来处理。区别在于本题额外引入了符号维度,让比较矩阵从一维变成了“符号 × 绝对值”的二维结构。


七、小结

实现Comparator的关键可以浓缩为四句话:

  1. 符号先行:用-${infer R}把输入拆成“符号 + 数字串”;
  2. 同号再看绝对值:同为负时对绝对值比较结果取反,同为非负时原样返回;
  3. 逐位决胜:去前导零后先比长度,长度相同再逐位比较,用占位串或映射表判断单字符大小;
  4. 用测试闭环:借助 utils/index.d.ts 的Equal/Expect与 test-cases.ts 中的 30+ 组断言(含正负、零、大整数、小数)验证每一种符号组合。

掌握这套“符号 + 字符串逐位”模板后,你可以继续挑战 Sum、MinusOne 等更复杂的类型级算术题,也可以把同样的模式迁移到 JSON 解析、URL 参数解析等其他 extreme 字符串题目中。

  • 示例工程

【免费下载链接】type-challenges

Collection of TypeScript type challenges with online judge

项目地址:https://gitcode.com/GitHub_Trending/ty/type-challenges
点击查看免费下载
上一篇:code-review-graph 图谱构建实战:用 build-graph Skill 初始化与增量维护本地代码知识图谱
下一篇:Weex无障碍测试完整指南:TalkBack与VoiceOver实战验证

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 9:37:03

Codex长期记忆难接?MCP桥接TencentDB Agent Memory实战

这段时间我在做一件听起来顺理成章、做起来却很折磨人的事&#xff1a;把腾讯云 TencentDB Agent Memory 当作 Codex CLI 的长期记忆后端&#xff0c;让同一个编码 Agent 在不同会话之间记得项目背景、记得上次改过哪几个文件、记得已经踩过的坑。单元测试里记忆写入和向量检索…

作者头像 李华
网站建设 2026/10/1 9:36:50

linux-command 命令详解:grpunconv 关闭群组投影密码的原理与实战

文档教程 【免费下载链接】linux-command Linux命令大全搜索工具&#xff0c;内容包含Linux命令手册、详解、学习、搜集。https://git.io/linux 项目地址&#xff1a; https://gitcode.com/GitHub_Trending/linux/linux-command 点击查看 免费下载 grpunconv 是 Linux 影子密码…

作者头像 李华
网站建设 2026/10/1 9:34:46

端侧视觉检索:TensorFlow.js与Web Worker实现零云端成本本地图片搜索

做端侧视觉检索这几个月&#xff0c;我最大的体会是&#xff1a;真正的隐私不是“上云后加密”&#xff0c;而是“根本不上云”。标题里那句话——“0 云端成本与 100% 隐私安全”——听起来有点营销味&#xff0c;但它确实把我做的一个东西概括完了&#xff1a;用 TensorFlow.…

作者头像 李华
网站建设 2026/10/1 9:33:30

systemd CPUAffinity覆盖taskset:容器绑核失效与CPU争抢排查

1. 现场还原&#xff1a;一条 systemd 命令让两个容器在同一个核上硬碰硬干这行久了&#xff0c;你会遇到一种特别有意思的故障&#xff1a;systemd 命令一执行&#xff0c;容器里的 CPU 使用率开始“打架”&#xff0c;进程明明之前绑核正常&#xff0c;重启之后就错位了。我遇…

作者头像 李华