- 示例工程
【免费下载链接】type-challenges
Collection of TypeScript type challenges with online judge
本篇文章围绕 type-challenges 仓库中的中等难度题目 00529「Absolute」展开,详细解析其题目要求、测试用例与标准实现方案。题目要求实现一个接收string | number | bigint的类型工具Absolute,将输入转换为"正数字符串"。读完本文,你将掌握模板字面量类型(template literal types)配合infer的模式匹配技巧,理解数字、字符串与 bigint 三类输入在类型层统一处理的方法,并学会从仓库测试用例反推类型行为边界。
题目背景:仓库中的 00529「Absolute」
本题目录为 questions/00529-medium-absolute,是 type-challenges 精选类型挑战中的一道中等难度题。从 info.yml 可以看到它的元信息:
- 难度:
medium - 标签:
math, template-literal(数学运算 + 模板字面量) - 作者:Andrey Krasovsky(GitHub:
bre30kra69cs)
题目同时提供 英文版 README、简体中文版、日文版 与 韩文版,题目描述一致。
题目要求
以日文版 README 为例,原题表述为:"string、number 或 bigint を受け取り、正の数を出力するAbsolute型を実装します"(实现一个接收 string、number 或 bigint,并输出正数的Absolute类型)。英文版更精确地补充了输出形式:"The output should be a positive number string"——输出必须是一个正数字符串。
原文档给出的示例:
type Test = -100; type Result = Absolute<Test>; // expected to be "100"注意三个关键点:
- 输入类型:
number、string、bigint三种均可; - 输出类型:不是数字类型
100,而是字符串字面量类型"100"; - 绝对值语义:负数要去掉负号,正数保持不变。
template.ts 给出了起始模板,类型参数约束已就位,但实现体为any,等待填答:
type Absolute<T extends number | string | bigint> = any测试用例驱动的需求全景
题目仓库中真正决定"什么算通过"的是 test-cases.ts。它通过Expect<Equal<...>>断言逐一校验实现结果,完整覆盖了三类输入与各种边界情况:
import type { Equal, Expect } from '@type-challenges/utils' type cases = [ Expect<Equal<Absolute<0>, '0'>>, Expect<Equal<Absolute<-0>, '0'>>, Expect<Equal<Absolute<10>, '10'>>, Expect<Equal<Absolute<-5>, '5'>>, Expect<Equal<Absolute<'0'>, '0'>>, Expect<Equal<Absolute<'-0'>, '0'>>, Expect<Equal<Absolute<'10'>, '10'>>, Expect<Equal<Absolute<'-5'>, '5'>>, Expect<Equal<Absolute<-1_000_000n>, '1000000'>>, Expect<Equal<Absolute<9_999n>, '9999'>>, ]把这 10 个用例整理成表,可以清晰看到必须覆盖的行为:
| 输入类型 | 用例 | 期望输出 | 行为要点 |
|---|---|---|---|
| number | 0 | '0' | 正数原样转为字符串 |
| number | -0 | '0' | 数字 -0 在类型层与 0 等价 |
| number | 10 | '10' | 正数原样 |
| number | -5 | '5' | 负数去除负号 |
| string | '0' | '0' | 字符串正数原样 |
| string | '-0' | '0' | 字符串负号需剥离 |
| string | '10' | '10' | 字符串正数原样 |
| string | '-5' | '5' | 字符串负数剥离负号 |
| bigint | -1_000_000n | '1000000' | 负数 + 数字分隔符展开 |
| bigint | 9_999n | '9999' | 正数 + 分隔符展开 |
值得注意的边界事实有两点:
-0与'-0'结果相同但成因不同:TypeScript 在类型层面不存在独立的-0字面量类型,-0与0等同,直接转字符串就是'0';而字符串字面量'-0'保留了负号,需要靠模式匹配剥离,结果同样是'0'。- bigint 的数字分隔符会被展开:输入中写的
1_000_000n(数字分隔符是合法 TS 语法)在转换后得到的是'1000000'而非'1_000_000',说明类型系统在将 bigint 字面量转为模板字面量时执行了与运行时String(bigint)一致的十进制规范化。
断言工具Equal与Expect来自仓库的 utils/index.d.ts,其中Equal<X, Y>采用经典的函数逆变比较法精确判断两个类型是否完全一致:
export type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false export type Expect<T extends true> = T这意味着实现结果的字面量形式必须与期望完全一致(例如'5'而非number),这也解释了为什么所有输出都是精确的字符串字面量类型。
标准解法:模板字面量 + infer 模式匹配
利用模板字面量类型,实现只需三行:
type Absolute<T extends number | string | bigint> = `${T}` extends `-${infer U}` ? U : `${T}`逐层拆解
第一步:统一输入形态。`${T}`是模板字面量类型,它能把number、bigint字面量以及string字面量统一转换为字符串字面量类型:
Absolute<10>中${10}得到'10';Absolute<-5>中${-5}得到'-5';Absolute<-1_000_000n>中${-1_000_000n}得到'-1000000';- 字符串输入则原样保留,
Absolute<'-5'>中${'-5'}仍是'-5'。
第二步:模式匹配剥离负号。用`-${infer U}`对转换后的字符串做模式匹配:若字符串以-开头,则用infer U捕获负号之后的部分并直接返回;若不匹配(没有负号),则退回返回${T}。
这里infer在条件类型中承担"类型层面的解构赋值":它从模板字面量的结构中推断出剩余部分U,这正是解决本类字符串处理问题的核心手法。
对照测试用例验证
Absolute<0>→${0}为'0',不匹配`-${infer U}`,返回'0'✓Absolute<-0>→ 类型层与0等价,同上返回'0'✓Absolute<-5>→'-5'匹配,U = '5',返回'5'✓Absolute<'-5'>→ 字符串'-5'匹配,U = '5',返回'5'✓Absolute<'-0'>→ 匹配-${infer U},U = '0',返回'0'✓Absolute<-1_000_000n>→${-1_000_000n}规范化为'-1000000',匹配后返回'1000000'✓Absolute<9_999n>→${9_999n}规范化为'9999',不匹配负号分支,返回'9999'✓
全部 10 个用例均可通过。
解法变体
将负号匹配直接放在输入上也是可行的,因为string是约束的一部分,负号只可能出现在字符串或转换为字符串后的数字中:
type Absolute<T extends number | string | bigint> = T extends `-${infer U}` ? U : `${T}`但要注意此写法中T若为number/bigint,先要能参与模板匹配(TS 会将其视为字面量参与匹配),语义上与标准写法等价;显式写`${T}`的做法则更清晰地传达"先统一转字符串再匹配"的意图,也更容易让阅读者理解。
此外,既然目标是"剥掉一个前导负号",本解法不依赖递归即可完成——这符合题目的 medium 定位;如果输入被定义为任意深度嵌套的字符串则才需要考虑递归展开,本题目不需要。
深入理解:模板字面量类型的转换规则
这道题之所以归入template-literal标签,是因为它的核心引擎是模板字面量类型。可以从 guides/infer.md 与 guides/recursive.md 等仓库指南进一步延伸学习。
结合本题目可以沉淀以下类型行为事实(均可由 test-cases.ts 的断言反推确认):
- number 字面量转字符串:
`${5}`得到'5',负号保留(`${-5}`得到'-5'),且-0与0在类型层无区分,统一得到'0'。 - bigint 字面量转字符串:等价于十进制表示的规范化字符串,源码中的数字分隔符(underscore)被展开移除。
- string 字面量保持原样:
`${'-0'}`仍是'-0',字符串中负号的信息得以保留,因此必须依赖匹配剥离。 - 模板字面量 +
infer可做"前缀剥除":T extends `-${infer R}` ? R : ...是字符串前缀匹配的标准范式,与TrimLeft、Replace等题目(如仓库中的 00106-medium-trimleft)共享同一套方法论。
如何在本地验证你的实现
type-challenges 仓库采用 pnpm workspace 组织,测试基于 TypeScript 编译器断言完成。你可以按以下流程在本地运行本题的测试:
git clone https://gitcode.com/GitHub_Trending/ty/type-challenges cd type-challenges pnpm install随后编辑 questions/00529-medium-absolute/template.ts,将any替换为你的实现,再用 TypeScript 编译器检查 test-cases.ts 是否无类型错误——当所有Expect<Equal<...>>断言成立时,编译即通过。仓库的 package.json 中还提供了pnpm lint等脚本辅助校验,测试工具类型Equal/Expect定义在 utils/index.d.ts 中,可随时查阅。
小结
Absolute是模板字面量类型入门到进阶的一座里程碑:它同时考验类型参数约束、字面量统一转换、infer模式匹配与边界情况意识。标准答案`${T}` extends `-${infer U}` ? U : `${T}`只有短短一行,却完整覆盖了 number、string、bigint 三类输入,以及-0、数字分隔符等容易被忽视的边界。对照 test-cases.ts 的 10 条断言逐项验证,你就能确认自己的实现真正符合题目要求——这套"先读用例、再写类型、最后逐条验证"的方法论,同样适用于仓库中其余 600 余道类型挑战。
- 示例工程
【免费下载链接】type-challenges
Collection of TypeScript type challenges with online judge
相关推荐
type-challenges 中等题精解:用模板字面量类型实现 `Capitalize<T>`
type challenges 中等题精解:用模板字面量类型实现 Capitalize<T 本篇文章基于开源仓库 type challenges(Collect
示例工程Front-End Checklist 中的 const/let 规则:告别 var,用块级作用域与 lint 消除提升导致的隐患
Front End Checklist 中的 const/let 规则:告别 var,用块级作用域与 lint 消除提升导致的隐患 本文围绕 Front End
示例工程TypeScript 类型挑战:type-challenges 题 112 之 CapitalizeWords 模板字面量类型实战解析
TypeScript 类型挑战:type challenges 题 112 之 CapitalizeWords 模板字面量类型实战解析 本文以 question
示例工程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考