跳到主要内容

第 15 章 · 字符串体操

读完本章你会
  • 用「模板 + infer」把字符串逐字拆开,再按规则重组
  • 实现 Split / Join:字符串与元组的双向转换
  • 实现命名风格转换:KebabCaseCamelCase
  • 实现 StartsWith / EndsWith 判定
  • 掌握「大小写边界」「下划线处理」这类字符串解析的细节陷阱
核心心法

字符串在类型层面就是一个字符元组。一切字符串体操 = 「按模式拆 → 递归处理 → 按规则拼回」。第 8 章打的基础,本章用到极致。


15.1 逐字解析:把字符串当元组遍历

15.1.1 拆单个字符

type Chars<S extends string> =
S extends `${infer F}${infer Rest}` ? [F, ...Chars<Rest>] : []

type A = Chars<'abc'> // ['a', 'b', 'c']

${infer F}${infer Rest} 中,F一个字符Rest 取剩余——这是字符串递归的「拆一步」。

特殊字符与 emoji

模板里的 infer FUTF-16 码元拆。emoji(如 😎 占两个码元)会被拆成两半。处理这类字符时要注意——第 1-8 章的 Capitalize 测试里就专门有 emoji 用例。

15.1.2 逐字符变换

// 全大写
type Upper<S extends string> =
S extends `${infer F}${infer Rest}`
? `${Uppercase<F>}${Upper<Rest>}`
: S

type A = Upper<'hello'> // 'HELLO'

15.2 Split 与 Join:字符串 ⇄ 元组

15.2.1 Split:按分隔符拆成元组

type Split<S extends string, SEP extends string = never> =
S extends `${infer L}${SEP}${infer R}`
? [L, ...Split<R, SEP>]
: [S]

type A = Split<'a-b-c', '-'> // ['a', 'b', 'c']
type B = Split<'a-b-c', ''> // ['a', '-', 'b', '-', 'c'](空分隔符逐字符)
02822 Split 的两个坑
  1. 默认参数Split<'abc'> 不带分隔符时,SEP 需要默认值,否则报「缺参数」。本书用 = never(表示「没有分隔符」),此时整个字符串作为一项返回。
  2. 空分隔符Split<'abc', ''> 要逐字符拆,但 '' 作为分隔符在模板里行为特殊('' 会匹配空、导致死循环)。所以必须先把 SEP 为空的情况单独处理——02822 的测试专门卡这个,参考答案里也专门有 SEP extends '' 的分支。

15.2.2 Join:元组拼成字符串

type Join<T extends unknown[], U extends string | number = ','> =
T extends [infer F, ...infer R]
? F extends string | number
? R extends []
? `${F}`
: `${F}${U}${Join<R, U>}`
: ''
: ''

type A = Join<['a', 'b', 'c'], '-'> // 'a-b-c'
type B = Join<['a', 'b', 'c']> // 'a,b,c'(默认分隔符)
type C = Join<[], '-'> // ''
Join 的细节
  • F extends string | number:元素必须是能拼进字符串的类型
  • R extends []:最后一项后面不加分隔符
  • 空数组返回 ''

15.3 命名风格转换:KebabCase / CamelCase

15.3.1 KebabCase:大写字母 → -小写

'FooBarBaz''foo-bar-baz'。核心是识别「大写字母」并插入 -

type KebabCase<S extends string> =
S extends `${infer C}${infer R}`
? R extends Uncapitalize<R> // R 首字符是小写?
? `${Uncapitalize<C>}${KebabCase<R>}` // 是 → 不加 -
: `${Uncapitalize<C>}-${KebabCase<R>}` // 否 → R 以大写开头,加 -
: S

type A = KebabCase<'FooBarBaz'> // 'foo-bar-baz'
type B = KebabCase<'ABC'> // 'a-b-c'
判断「大写边界」的秘诀

R extends Uncapitalize<R>:把 R 的首字母转小写,若 R 没变(R 首字符本来就小写或非字母),说明没有边界;若变了(R 首字符是大写),说明这里该插 -。这是「识别大小写边界」的标准手法。

15.3.2 CamelCase:下划线 + 字母 → 大写

'foo_bar_hello''fooBarHello'。比 KebabCase 多一个「下划线后跟字母才是分隔」的判定:

// 判断一个字符是不是字母(第 8 章)
type IsLetter<C extends string> =
C extends Uppercase<C>
? C extends Lowercase<C> ? false : true
: true

type CamelCase<S extends string> =
S extends `${infer F}${infer R}`
? F extends '_'
? R extends `${infer N}${infer T}`
? IsLetter<N> extends true
? `${Capitalize<N>}${CamelCase<T>}` // _ + 字母 → 吃掉 _,大写字母
: `${F}${CamelCase<R>}` // _ + 非字母 → 保留 _
: F
: `${Lowercase<F>}${CamelCase<R>}` // 普通字符 → 小写,继续
: S

type A = CamelCase<'foo_bar'> // 'fooBar'
type B = CamelCase<'foo__bar'> // 'foo_Bar'(双下划线只吃掉一个)
CamelCase 的经典陷阱

'foo__bar' 应该得到 'foo_Bar'——第一个 _ 后面跟的是 _(非字母),所以保留;第二个 _ 后面跟 b(字母),所以吃掉并大写。这就是 IsLetter 判定存在的原因。00114 CamelCase 专门卡这个边界。


15.4 StartsWith / EndsWith

第 8 章写过,这里是完整版(含 '' 边界):

type StartsWith<T extends string, U extends string> =
T extends `${U}${infer R}` ? true : false

type EndsWith<T extends string, U extends string> =
T extends `${infer L}${U}` ? true : false

type A = StartsWith<'foobar', 'foo'> // true
type B = StartsWith<'foobar', ''> // true(空串是任何串的前缀)
type C = EndsWith<'foobar', 'bar'> // true
type D = EndsWith<'foobar', ''> // true

本章小结

  • 字符串 = 字符元组;${infer F}${infer R} 是「拆一步」
  • Split / Join 做字符串 ⇄ 元组转换,注意空分隔符与默认参数两个坑
  • KebabCase 用「R 是否以大写开头」插 -CamelCaseIsLetter 区分「_ + 字母」与「_ + 非字母」
  • StartsWith / EndsWith 就是模板匹配,'' 是任何串的前缀/后缀

本章练习

挑战题

题号题目难度考察点
02822Splithard空分隔符 + 默认参数
05310Joinmedium元组拼串
00612KebabCasemedium大写边界
00114CamelCasehard下划线 + IsLetter
02688StartsWithmedium前缀匹配
02693EndsWithmedium后缀匹配

代码练习

  1. Chars:实现 type Chars<S> 把字符串拆成字符元组(15.1.1),验证 Chars<'hi'>['h', 'i']

  2. Upper / Lower:分别实现 UpperLower(逐字符 + Uppercase/Lowercase),验证 emoji 会被拆成两段(观察现象即可)。

  3. Split 变体:给 Split 处理 Split<''>(空串无分隔符)返回 ['']Split<'', ''> 返回 [](参考 02822 的边界)。

  4. KebabCase 练习:实现 KebabCase,验证 'fooBarBaz''foo-bar-baz''Foo-Bar''foo--bar'(连字符会被保留导致双横线,观察这个细节)。

  5. CamelCase 反向:实现 type KebabToCamel<S>'foo-bar''fooBar'),提示:先按 - 拆,再逐段 Capitalize 拼接。