Absolute
大约 3 分钟
Absolute
题目
实现 Absolute 类型,接受 string、number 或 bigint,输出一个正数字符串。
type Test = -100
type Result = Absolute<Test> // 期望结果:"100"解答
type Absolute<T extends number | string | bigint> =
`${T}` extends `-${infer N}` ? N : `${T}`一行搞定:先转成字符串,再把开头的负号去掉(如果有的话)。
分步解析
第一步 — 用模板字面量统一转成字符串
`${T}`TypeScript 的模板字面量类型可以直接插值 number、string、bigint,将它们的字面量类型转换为字符串字面量:
type A = `${-100}` // "-100"
type B = `${100}` // "100"
type C = `${-42n}` // "-42"
type D = `${"-100"}` // "-100"这一步统一处理了三种输入类型,无需分别讨论。
第二步 — 模式匹配,检测开头的负号
`${T}` extends `-${infer N}` ? N : `${T}`- 如果字符串以
-开头,TypeScript 把剩余部分推断为N(负号之后的部分),直接返回N— 等于去掉了负号。 - 如果不以
-开头,说明已经是非负数,直接返回`${T}`不变。
所有测试用例演算
| 输入 | `${T}` | 匹配 -${N}? | 结果 |
|---|---|---|---|
-100 | "-100" | ✅ N = "100" | "100" |
100 | "100" | ❌ | "100" |
"-100" | "-100" | ✅ N = "100" | "100" |
"100" | "100" | ❌ | "100" |
-42n | "-42" | ✅ N = "42" | "42" |
0 | "0" | ❌ | "0" |
深入理解
为什么返回值必须是字符串?
你可能会期望 Absolute<-100> 返回数字 100 而不是字符串 "100"。原因在于 TypeScript 类型系统的局限:去掉负号本质上是字符串操作,模板字面量 infer 模式只能返回字符串。如果要把结果再转回数字类型,需要用 tuple 长度技巧,且对大数字会失效 — 得不偿失。
模板字面量:不只是拼接,更是字符串解析工具
这道题展示了模板字面量类型的另一面——不只能拼接字符串,还能解析和提取字符串的一部分:
// 提取路径第一段
type Head<S extends string> = S extends `${infer H}/${string}` ? H : S
// 检测前缀
type StartsWithA<S extends string> = S extends `A${string}` ? true : false
// 去掉开头的负号
type StripMinus<S extends string> = S extends `-${infer R}` ? R : S模板字面量中的 infer 是关键,可以提取匹配到的任意子串。
约束 T extends number | string | bigint
为什么要包含 bigint?因为 JavaScript 的 BigInt 字面量(如 -42n)在 TypeScript 中是独立的原始类型。不加 bigint 的话,传入 bigint 类型会报错。模板字面量插值三者都支持,所以约束范围与能力一致。
-0 怎么处理?
负零 (-0) 是 JavaScript 的一个特殊值。在 TypeScript 类型系统中,-0 作为数字字面量类型通常被视为 0,所以 Absolute<-0> 得到 "0" — 结果是正确的。
为什么不用数值比较的方式?
你可能想到 T > 0 ? T : -T 这样的写法,但 TypeScript 类型层面的数值运算极为有限,无法直接对任意数字字面量取反。字符串模式匹配是目前最简洁、最通用的解法。
关键要点
- 模板字面量可以在类型层面将
number、string、bigint统一转换成字符串 — 极其实用的归一化技巧 `-${infer N}`模式是类型层面去掉(或检测)开头负号的标准写法- 返回值永远是字符串,因为去负号是字符串操作;要转回数字需要复杂的 tuple 运算
- 这道题是学习模板字面量作为解析工具的绝佳入门案例,而不只是拼接
- 初始的字符串强制转换让解法能统一处理三种输入类型,避免了重复的条件分支
