Yuzhe's Blog

yuzhes

驼峰命名 CamelCase

驼峰命名 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}`

模板字面量推断把字符串在第一个 _ 处拆分为三部分:

例如:'hello_world_with_types'Head = 'hello'C = 'w'Tail = 'orld_with_types'

第二步 — 变换并递归

`${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。这比只拆分成两部分更精确:

这避免了一个常见陷阱:递归中的 Lowercase 破坏了上一层递归中 Capitalize 的效果。

为什么需要 Lowercase<Head>

如果不小写处理,全大写输入 'HELLO_WORLD' 会变成 'HELLOWorld' 而不是 'helloWorld'Lowercase<Head> 会规范化每段下划线前的内容。

边界情况

连续下划线: 'foo__bar'

前导下划线: '_foo_bar'

无下划线: '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 的模板字面量类型结合递归和条件类型,构成了一个图灵完备的字符串处理系统。你可以在类型层面实现几乎任何字符串变换。

核心要点