字符串分割 Split
大约 3 分钟TC-Hard
字符串分割 Split
题目
在类型系统中实现 Split,复制 JavaScript 的 String.prototype.split() 行为。
type result = Split<'Hi! How are you?', ' '>
// 期望结果:['Hi!', 'How', 'are', 'you?']解答
type Split<
S extends string,
SEP extends string
> = string extends S
? string[]
: S extends `${infer Head}${SEP}${infer Tail}`
? [Head, ...Split<Tail, SEP>]
: SEP extends ''
? []
: [S]核心思路:递归匹配字符串中的分隔符,将分隔符前的部分累积到元组中,并处理空分隔符和泛型 string 的边界情况。
逐步拆解
第一步 — 处理泛型 string 类型
string extends S ? string[] : ...如果 S 是宽泛的 string 类型(非字面量),无法在类型层面进行分割。返回 string[] 以匹配运行时行为。
第二步 — 匹配并在分隔符处拆分
S extends `${infer Head}${SEP}${infer Tail}`
? [Head, ...Split<Tail, SEP>]
: ...模板字面量推断找到 S 中 SEP 的第一次出现:
Head= 分隔符之前的所有内容Tail= 分隔符之后的所有内容
将 Head 放入元组,然后对 Tail 递归。
第三步 — 基础情况
SEP extends '' ? [] : [S]当不再匹配到分隔符时:
- 如果
SEP是''(空字符串):返回[],因为最后一个字符已经被消费了 - 否则:
S是最后一段——返回[S]
推导过程:
Split<'Hi! How are you?', ' '>
第 1 轮:Head = 'Hi!',Tail = 'How are you?'
→ ['Hi!', ...Split<'How are you?', ' '>]
第 2 轮:Head = 'How',Tail = 'are you?'
→ ['How', ...Split<'are you?', ' '>]
第 3 轮:Head = 'are',Tail = 'you?'
→ ['are', ...Split<'you?', ' '>]
第 4 轮:'you?' 不包含 ' '
→ ['you?']
展开:['Hi!', 'How', 'are', 'you?'] ✓深入分析
空字符串分隔符
当 SEP = '' 时,分割应该产生单个字符,就像 'abc'.split('') → ['a', 'b', 'c']:
Split<'abc', ''>
第 1 轮:'abc' extends `${infer Head}${infer Tail}`
Head = 'a',Tail = 'bc'
→ ['a', ...Split<'bc', ''>]
第 2 轮:Head = 'b',Tail = 'c'
→ ['b', ...Split<'c', ''>]
第 3 轮:Head = 'c',Tail = ''
→ ['c', ...Split<'', ''>]
第 4 轮:'' extends `${infer Head}${infer Tail}`?否(无法拆分空字符串)
→ SEP extends ''?是 → []
最终:['a', 'b', 'c'] ✓SEP extends '' ? [] 基础情况专门为此设计——当空分隔符递归到达空字符串时。
多字符分隔符
type T = Split<'a---b---c', '---'>
// ['a', 'b', 'c']模板字面量匹配将 SEP 作为完整子串处理,而非逐字符匹配。
连续分隔符
type T = Split<'a,,b,,c', ','>
// ['a', '', 'b', '', 'c']这与 JavaScript 行为一致:连续分隔符产生空字符串。
第 1 轮:Head = 'a',Tail = ',b,,c'
第 2 轮:Head = '',Tail = 'b,,c'
第 3 轮:Head = 'b',Tail = ',c'
第 4 轮:Head = '',Tail = 'c'
第 5 轮:'c' → ['c']
→ ['a', '', 'b', '', 'c'] ✓为什么用 string extends S 而不是 S extends string?
S extends string 总是为真(因为约束)。我们需要检测 S 是宽泛的 string 类型还是字符串字面量:
string extends 'hello' // false(string 比 'hello' 更宽)
string extends string // true所以 string extends S 只有当 S 本身就是 string(或更宽)时才为真,而不是特定字面量时。
与运行时 split() 的对比
| 行为 | 运行时 | 类型层 |
|---|---|---|
'a,b'.split(',') | ['a', 'b'] | ['a', 'b'] ✓ |
'abc'.split('') | ['a', 'b', 'c'] | ['a', 'b', 'c'] ✓ |
'a,,b'.split(',') | ['a', '', 'b'] | ['a', '', 'b'] ✓ |
'hello'.split(',') | ['hello'] | ['hello'] ✓ |
''.split(',') | [''] | [''] ✓ |
核心要点
- 模板字面量推断天然找到子串的第一次出现——非常适合实现
split - 递归元组构建
[Head, ...Split<Tail, SEP>]的方式与命令式实现 split 的逻辑一致 - 边界情况处理(空分隔符、泛型
string、无匹配)需要仔细设计基础情况 string extends S是检测S是宽泛string类型还是字面量的可靠方法- 这个模式展示了 TypeScript 类型系统可以复制非平凡的字符串操作
