驼峰命名 CamelCase
大约 3 分钟TC-Hard
驼峰命名 CamelCase
题目
实现 CamelCase<T>,将 snake_case 字符串转换为 camelCase。
type camelCase1 = CamelCase<'hello_world_with_types'>
// 期望结果:'helloWorldWithTypes'
type camelCase2 = CamelCase<'HELLO_WORLD_WITH_TYPES'>
// 期望结果:'helloWorldWithTypes'解答
type CamelCase<S extends string> =
S extends `${infer Head}_${infer C}${infer Tail}`
? `${Lowercase<Head>}${Uppercase<C>}${CamelCase<Tail>}`
: Lowercase<S>核心思路:在每个下划线处拆分为三部分——下划线前的内容小写、下划线后的第一个字符大写、剩余部分递归处理。
逐步拆解
第一步 — 在下划线处三段式拆分
S extends `${infer Head}_${infer C}${infer Tail}`模板字面量推断把字符串在第一个 _ 处拆分为三部分:
Head:下划线前的所有内容C:下划线后的单个字符Tail:剩余的字符串
例如:'hello_world_with_types' → Head = 'hello',C = 'w',Tail = 'orld_with_types'
第二步 — 变换并递归
`${Lowercase<Head>}${Uppercase<C>}${CamelCase<Tail>}`Lowercase<Head>规范化该段(处理全大写输入)Uppercase<C>创建驼峰的大写字母CamelCase<Tail>递归处理剩余的下划线
第三步 — 基础情况
: Lowercase<S>没有下划线时,整段小写返回。
推导过程:
S = 'hello_world_with_types'
Head = 'hello', C = 'w', Tail = 'orld_with_types'
→ 'hello' + 'W' + CamelCase<'orld_with_types'>
S = 'orld_with_types'
Head = 'orld', C = 'w', Tail = 'ith_types'
→ 'orld' + 'W' + CamelCase<'ith_types'>
S = 'ith_types'
Head = 'ith', C = 't', Tail = 'ypes'
→ 'ith' + 'T' + CamelCase<'ypes'>
S = 'ypes'(无下划线)
→ 'ypes'
最终:'hello' + 'W' + 'orld' + 'W' + 'ith' + 'T' + 'ypes'
= 'helloWorldWithTypes' ✓全大写输入 'HELLO_WORLD_WITH_TYPES' 同样有效:
Head = 'HELLO' → Lowercase → 'hello'
C = 'W' → Uppercase → 'W'
Tail = 'ORLD_WITH_TYPES' → 递归...
→ 'hello' + 'W' + 'orld' + 'W' + 'ith' + 'T' + 'ypes'
= 'helloWorldWithTypes' ✓深入分析
为什么要三段式推断 ${Head}_${C}${Tail}?
关键洞察在于推断下划线后的单个字符 C。这比只拆分成两部分更精确:
${Head}_${Tail}— 需要单独把Tail的首字母大写,但下一层递归的Lowercase会破坏掉这个大写${Head}_${C}${Tail}—C已经被隔离出来,直接Uppercase<C>即可,Tail不会经过Lowercase直到它自己被拆分
这避免了一个常见陷阱:递归中的 Lowercase 破坏了上一层递归中 Capitalize 的效果。
为什么需要 Lowercase<Head>
如果不小写处理,全大写输入 'HELLO_WORLD' 会变成 'HELLOWorld' 而不是 'helloWorld'。Lowercase<Head> 会规范化每段下划线前的内容。
边界情况
连续下划线: 'foo__bar'
Head = 'foo',C = '_',Tail = 'bar'Uppercase<'_'>='_'(非字母,不变)- 结果:
'foo_Bar'— 保留一个下划线
前导下划线: '_foo_bar'
Head = '',C = 'f',Tail = 'oo_bar'- 结果:
'' + 'F' + CamelCase<'oo_bar'>→'FooBar'
无下划线: 'hello'
- 进入基础情况 →
'hello'
另一种方法:两段式拆分(为什么行不通)
常见的第一次尝试使用两段式拆分:
// ❌ 有 Bug:Lowercase 会破坏前一层的 Capitalize
type CamelCase<S extends string> =
S extends `${infer Head}_${infer Tail}`
? `${Lowercase<Head>}${CamelCase<Capitalize<Lowercase<Tail>>>}`
: Lowercase<S>这个方案失败是因为在递归处理 'World_with_types' 时,Lowercase<Head> 把 'World' 变回了 'world',丢失了上一层 Capitalize 施加的大写。三段式拆分完全避免了这个问题。
模板字面量类型是图灵完备的
这道题展示了 TypeScript 的模板字面量类型结合递归和条件类型,构成了一个图灵完备的字符串处理系统。你可以在类型层面实现几乎任何字符串变换。
核心要点
- 三段式模板推断(
${A}_${B}${C})可以隔离单个字符进行变换,不受后续递归影响 Lowercase和Uppercase是内置的固有类型——用于大小写转换- 递归模板字面量类型可以逐字符或逐段处理字符串
- 注意递归破坏问题——一层的变换被下一层的处理所撤销,这是常见 Bug
