百分比解析器
百分比解析器
题目
实现 PercentageParser<T extends string>。
根据正则 /^(\+|\-)?(\d*)?(\%)?$/ 匹配 T,并提取三个捕获组。
结构为:[符号, 数字, 单位]
未捕获的组默认为空字符串 ''。
type PString1 = ""
type PString2 = "+85%"
type PString3 = "-85%"
type PString4 = "85%"
type PString5 = "85"
type R1 = PercentageParser<PString1> // ['', '', '']
type R2 = PercentageParser<PString2> // ['+', '85', '%']
type R3 = PercentageParser<PString3> // ['-', '85', '%']
type R4 = PercentageParser<PString4> // ['', '85', '%']
type R5 = PercentageParser<PString5> // ['', '85', '']解法
最简洁的写法是一条嵌套的条件类型链:
type PercentageParser<T extends string> =
T extends `${'+' | '-'}${infer Rest}`
? T extends `${infer Sign}${Rest}`
? Rest extends `${infer Num}%`
? [Sign, Num, '%']
: [Sign, Rest, '']
: ['', '', '']
: T extends `${infer Num}%`
? ['', Num, '%']
: ['', T, '']逐步拆解
第一步 — 检测可选符号
T extends `${'+' | '-'}${infer Rest}`如果 T 以 + 或 - 开头,就匹配成功,并将剩余部分捕获为 Rest。'+' | '-' 是联合类型,TypeScript 会对它分发——相当于 OR 逻辑。
第二步 — 提取符号字符
T extends `${infer Sign}${Rest}`已知 T 以符号开头,而 Rest 是已知的具体类型,TypeScript 可以将 Sign 精确锁定为第一个字符。
第三步 — 检测可选 % 后缀
Rest extends `${infer Num}%`如果剩余字符串以 % 结尾,提取数字部分 Num,单位为 '%';否则单位为 ''。
第四步 — 处理无符号的情况
T extends `${infer Num}%`
? ['', Num, '%']
: ['', T, '']没有符号时,检查 T 是否以 % 结尾。是则返回 ['', Num, '%'];否则整个字符串都是数字部分,单位为空。
执行轨迹示例
PercentageParser<'+85%'>
→ 匹配 '+' | '-' 前缀 → Rest = '85%'
→ Sign = '+'
→ '85%' extends `${infer Num}%` → Num = '85'
→ ['+', '85', '%'] ✓
PercentageParser<'85%'>
→ 不匹配 '+' | '-' 前缀
→ '85%' extends `${infer Num}%` → Num = '85'
→ ['', '85', '%'] ✓
PercentageParser<''>
→ 不匹配 '+' | '-' 前缀
→ '' 不匹配 `${infer Num}%`
→ ['', '', ''] ✓深入理解
为什么模板字面量里的联合类型可以当 OR 用
TypeScript 在条件类型中会分发联合类型。在模板字面量里写 '+' | '-' 等价于:
"T 是否以
+开头,或者 以-开头?"
两个分支解析出的 Rest 类型相同(字符串的尾部),所以联合自然合并。
两步提取符号的技巧
你可能会问:既然已经匹配了 T extends \${'+' | '-'}${infer Rest}``,为什么不能直接拿到符号?
原因在于 '+' | '-' 是一个联合类型,而非 infer 捕获——无法直接命名它。解决方式是再做一次匹配:
T extends `${infer Sign}${Rest}`这里 Rest 已经是具体已知的类型,TypeScript 就能把 Sign 精确锁定为唯一的前缀字符。这是类型层面字符串解析的常见惯用写法。
辅助类型的写法
社区里也有人把三个捕获组分别拆成独立的辅助类型:
type GetSign<T extends string> =
T extends `${'+' | '-'}${string}` ? (T extends `${infer S}${string}` ? S : never) : ''
type GetUnit<T extends string> =
T extends `${string}%` ? '%' : ''
type GetNumber<T extends string,
S extends string = GetSign<T>,
U extends string = GetUnit<T>> =
T extends `${S}${infer N}${U}` ? N : ''
type PercentageParser<T extends string> =
[GetSign<T>, GetNumber<T>, GetUnit<T>]这种写法可读性更高(每个辅助类型只负责一件事),和正则 /^(\+|\-)?(\d*)?(\%)?$/ 的三个捕获组一一对应。
代价是:对 T 做了三次独立求值,在大规模项目中可能略微影响类型检查性能。
边界情况一览
| 输入 | 期望结果 | 说明 |
|---|---|---|
"" | ['', '', ''] | 空字符串,所有组默认 '' |
"+" | ['+', '', ''] | 只有符号,无数字无单位 |
"%" | ['', '', '%'] | 只有单位 |
"+%" | ['+', '', '%'] | 符号 + 单位,无数字 |
"-0%" | ['-', '0', '%'] | 负零百分比 |
与其他字符串解析题的关系
| 题目 | 技术要点 |
|---|---|
Trim (108) | 去除前缀/后缀字符 |
Replace (116) | 按子串分割 |
StartsWith (2688) | 模板字面量检测前缀 |
EndsWith (2693) | 模板字面量检测后缀 |
PercentageParser (1978) | 同时提取前缀 + 中间 + 后缀 |
Split (2822-hard) | 按分隔符递归分割 |
PercentageParser 是将前缀检测(StartsWith 风格)与后缀检测(EndsWith 风格)组合在单个类型表达式中的典型练习。
核心要点
- 模板字面量中的联合类型(
'+' | '-')在类型层面充当前缀匹配的 OR - 两步提取符号:先用联合检测前缀,再用
infer捕获具体字符——这是常见的类型层面解析模式 - 后缀检测(
T extends \${infer N}%`)和EndsWith` 原理相同,天然适合处理可选尾部字符 - 辅助类型拆分(每个捕获组一个类型)与正则捕获组一一对应,可读性极佳
- 此模式可推广到任何
[前缀, 主体, 后缀]结构的类型层面解析问题
