TypeChallenge 612: KebabCase
KebabCase
题目
将 camelCase 或 PascalCase 字符串转换为 kebab-case。
type FooBarBaz = KebabCase<"FooBarBaz">
// 期望: "foo-bar-baz"
type DoNothing = KebabCase<"do-nothing">
// 期望: "do-nothing"测试用例:
type cases = [
Expect<Equal<KebabCase<'FooBarBaz'>, 'foo-bar-baz'>>,
Expect<Equal<KebabCase<'fooBarBaz'>, 'foo-bar-baz'>>,
Expect<Equal<KebabCase<'foo-bar'>, 'foo-bar'>>,
Expect<Equal<KebabCase<'foo_bar'>, 'foo_bar'>>,
Expect<Equal<KebabCase<'Foo-Bar'>, 'foo--bar'>>,
Expect<Equal<KebabCase<'ABC'>, 'a-b-c'>>,
Expect<Equal<KebabCase<'-'>, '-'>>,
Expect<Equal<KebabCase<''>, ''>>,
Expect<Equal<KebabCase<'😎'>, '😎'>>,
]解答
type KebabCase<S extends string> = S extends `${infer First}${infer Rest}`
? Rest extends Uncapitalize<Rest>
? `${Uncapitalize<First>}${KebabCase<Rest>}`
: `${Uncapitalize<First>}-${KebabCase<Rest>}`
: S深入解析
核心思路
关键观察:我们需要在每个大写字母之前插入连字符(首字母除外),然后全部转小写。
检测大写字母的技巧是利用 TypeScript 的 Uncapitalize<T>,它会将字符串首字符转为小写。我们可以用它来比较:
// 如果 Rest 以大写开头,Rest !== Uncapitalize<Rest>
Rest extends Uncapitalize<Rest> // 如果 Rest 以大写开头则为 false逐步拆解
让我们追踪 KebabCase<'FooBar'> 的执行过程:
第一次迭代:
First = 'F',Rest = 'ooBar''ooBar' extends Uncapitalize<'ooBar'>→'ooBar' extends 'ooBar'→ ✅ true- 结果:
'f' + KebabCase<'ooBar'>
处理 'ooBar':
First = 'o',Rest = 'oBar''oBar' extends 'oBar'→ ✅ true- 结果:
'o' + KebabCase<'oBar'>
处理 'oBar':
First = 'o',Rest = 'Bar''Bar' extends Uncapitalize<'Bar'>→'Bar' extends 'bar'→ ❌ false- 结果:
'o' + '-' + KebabCase<'Bar'>
处理 'Bar':
First = 'B',Rest = 'ar''ar' extends 'ar'→ ✅ true- 结果:
'b' + KebabCase<'ar'>
处理 'ar':
First = 'a',Rest = 'r''r' extends 'r'→ ✅ true- 结果:
'a' + KebabCase<'r'>
处理 'r':
First = 'r',Rest = '''' extends ''→ ✅ true- 结果:
'r' + KebabCase<''>
基础情况:
KebabCase<''>返回''
最终: 'f' + 'o' + 'o' + '-' + 'b' + 'a' + 'r' + '' = 'foo-bar'
边界情况处理
已经是 kebab-case ('foo-bar'):
- 连字符
-不是大写字母,所以Uncapitalize<'-bar'>等于'-bar' - 不会插入额外的连字符
连续大写 ('ABC'):
- 每个大写字母都会触发连字符分支
'A'→'a',然后'B'是大写 →'-b',然后'C'是大写 →'-c'- 结果:
'a-b-c'
非字母字符 ('😎', '-'):
Uncapitalize对非字母没有影响- 原样通过
替代方案
有些解法使用 Lowercase 代替:
type KebabCase<S extends string> = S extends `${infer F}${infer R}`
? R extends Uncapitalize<R>
? `${Lowercase<F>}${KebabCase<R>}`
: `${Lowercase<F>}-${KebabCase<R>}`
: S两者在这里效果相同,因为我们每次只处理一个字符。对于单个字符,Uncapitalize 和 Lowercase 行为一致。
关键要点
Uncapitalize<T>作为大写检测器: 比较S extends Uncapitalize<S>可以判断字符串是否以小写(或非字母)开头递归模板字面量类型: 使用
${infer First}${infer Rest}逐字符处理字符串条件连字符插入: 在处理剩余部分之前插入连字符,而不是之后
内置字符串工具: TypeScript 的
Uppercase、Lowercase、Capitalize和Uncapitalize是字符串操作的利器边界情况处理: 该方案无需特殊处理就能自然应对空字符串、非字母和已格式化的字符串
