|
| 1 | +--- |
| 2 | +nav: |
| 3 | + title: Challenges |
| 4 | + path: /type-challenges |
| 5 | +group: |
| 6 | + title: Medium |
| 7 | + order: 2 |
| 8 | +title: KebabCase |
| 9 | +order: 36 |
| 10 | +--- |
| 11 | + |
| 12 | +# KebabCase |
| 13 | + |
| 14 | +### 要求 |
| 15 | + |
| 16 | +Implement `KebabCase<T>` which converts a camelCase or PascalCase string to kebab-case. |
| 17 | + |
| 18 | +For example: |
| 19 | + |
| 20 | +```ts |
| 21 | +type Result1 = KebabCase<'fooBarBaz'>; // expected: 'foo-bar-baz' |
| 22 | +type Result2 = KebabCase<'FooBarBaz'>; // expected: 'foo-bar-baz' |
| 23 | +type Result3 = KebabCase<'foo'>; // expected: 'foo' |
| 24 | +``` |
| 25 | + |
| 26 | +### 测试用例 |
| 27 | + |
| 28 | +```ts |
| 29 | +import { Equal, Expect } from '@type-challenges/utils'; |
| 30 | + |
| 31 | +type cases = [ |
| 32 | + Expect<Equal<KebabCase<'fooBarBaz'>, 'foo-bar-baz'>>, |
| 33 | + Expect<Equal<KebabCase<'FooBarBaz'>, 'foo-bar-baz'>>, |
| 34 | + Expect<Equal<KebabCase<'foo'>, 'foo'>>, |
| 35 | + Expect<Equal<KebabCase<'user2Name'>, 'user2-name'>>, |
| 36 | + Expect<Equal<KebabCase<'userName'>, 'user-name'>>, |
| 37 | + Expect<Equal<KebabCase<'getUserInfo'>, 'get-user-info'>>, |
| 38 | + Expect<Equal<KebabCase<'myAPIKey'>, 'my-a-p-i-key'>>, |
| 39 | + Expect<Equal<KebabCase<''>, ''>>, |
| 40 | +]; |
| 41 | +``` |
| 42 | + |
| 43 | +### 解析 |
| 44 | + |
| 45 | +这道题的核心思路是: |
| 46 | + |
| 47 | +1. **递归处理字符串**:逐个字符检查 |
| 48 | +2. **识别大写字母**:当遇到大写字母时,需要在前面添加 `-` 并将其转为小写 |
| 49 | +3. **边界处理**:字符串开头的大写字母不需要添加 `-` |
| 50 | + |
| 51 | +关键技术点: |
| 52 | +- 使用模板字面量类型进行模式匹配 |
| 53 | +- 使用 `Uncapitalize` 工具类型将首字母转小写 |
| 54 | +- 使用条件类型判断字符是否为大写字母 |
| 55 | +- 递归处理剩余字符串 |
| 56 | + |
| 57 | +实现步骤: |
| 58 | +1. 首先将首字母转为小写(使用 `Uncapitalize`) |
| 59 | +2. 递归处理剩余部分,遇到大写字母时添加 `-` 并转小写 |
| 60 | +3. 空字符串作为递归终止条件 |
| 61 | + |
| 62 | +### Answer |
| 63 | + |
| 64 | +```ts |
| 65 | +// 辅助类型:将字符串转为 kebab-case(递归部分) |
| 66 | +type KebabCaseHelper<S extends string> = S extends `${infer First}${infer Rest}` |
| 67 | + ? First extends Uppercase<First> // 判断是否为大写字母 |
| 68 | + ? First extends Lowercase<First> // 排除数字和特殊字符(它们的大小写相同) |
| 69 | + ? `${First}${KebabCaseHelper<Rest>}` // 不是字母,直接拼接 |
| 70 | + : `-${Lowercase<First>}${KebabCaseHelper<Rest>}` // 是大写字母,添加 - 并转小写 |
| 71 | + : `${First}${KebabCaseHelper<Rest>}` // 小写字母直接拼接 |
| 72 | + : S; |
| 73 | + |
| 74 | +// 主类型:处理首字母并调用辅助类型 |
| 75 | +type KebabCase<S extends string> = S extends `${infer First}${infer Rest}` |
| 76 | + ? `${Uncapitalize<First>}${KebabCaseHelper<Rest>}` |
| 77 | + : S; |
| 78 | +``` |
| 79 | + |
| 80 | +### 知识点 |
| 81 | + |
| 82 | +- **模板字面量类型(Template Literal Types)**:使用 `${infer First}${infer Rest}` 进行字符串模式匹配 |
| 83 | +- **条件类型(Conditional Types)**:`T extends U ? X : Y` 进行类型判断 |
| 84 | +- **内置工具类型**: |
| 85 | + - `Uppercase<T>`:将字符串字面量类型转为大写 |
| 86 | + - `Lowercase<T>`:将字符串字面量类型转为小写 |
| 87 | + - `Uncapitalize<T>`:将首字母转为小写 |
| 88 | +- **递归类型**:类型定义中引用自身来处理不定长度的字符串 |
| 89 | + |
| 90 | +### 扩展思考 |
| 91 | + |
| 92 | +这道题还可以扩展为: |
| 93 | +1. **SnakeCase**:将驼峰转为蛇形命名(`foo_bar_baz`) |
| 94 | +2. **CamelCase**:将 kebab-case 转为驼峰命名 |
| 95 | +3. **PascalCase**:将字符串转为帕斯卡命名 |
| 96 | + |
| 97 | +### tip |
| 98 | + |
| 99 | +- [playground](https://www.typescriptlang.org/play?#code/PQKgUABBBMELQQJYGcoE8AOAnApmgRwBcALAewFcACAGwCMB7OegIx2CIDsBzKRqgJQQAxAFtkXAJakipAE74A5sqWkqAK2QBGalQDWyRtXoAzZM0KkKAGmQBDAK4AOSrP0AeeVAA8t0MAXv-UBqcKgEHRqAq0xAhZCojQKAv-qA6PKgPJyoBJsqAwPKgIjyoAw8qAhK6gG9yoDe8qAEXKgIPqGmpaAkHKgBdyoAXcqAhxKIhraAz7KgKDy-jj4hlLYCAAA4ewAJk0AZzUQBl7QAsMfMhiIAa9cARbuhMRV6oCq8qA6ov2JcVAZHlQA15UBkeVAHHlQHx5K0dAIwVQDcDJzkq0AOqgOpyoCY8qApPKgBTyoD+CoC08qA-gqAoPKgEYKoAU8qAmfKgJjyoC08qAZgqgBTyoAU8qA1PKgPYKoBCCoC2CqAXK6gJYKK6A6nIN3h6gCoKoBmCsN3R6AFvKgBTyoCCDo8kx6AxPKgITyoB88qAfq6A2PJfJyASQrU8tgFrymqAkAqgP4K5xU3QCaFUBweVAOQUbqpFR6AFPKgODyTyengByCoAqCs93O5-J4Af59AeHk3hpAJIVQEp5UBneXOFiBYOIiKhEOAADIBgOJDlJJMMNQDQ8qIroAAckAr0BYACpJAAabLMvC86DXpjXRtAi9AdHlQDMFA8JYAkRWb7c-oB15UAkDpuBWJ5eLXgFkAM51-0AxgAAqQBAAC2sDAAFsFj7ECB1Aoe0EGNgAHk-AAI1cABHABVfxfAAt2IAAtJ1AdwR3xAwAGZAIccC0NnFhEAgnDB0ANiQ2dAGwkNcANcAAtQPTQABxDxBxDYA0Cg3c92PU9j1PEA7xXABJI90JfBckL3fclw3dDd0vfCbwfW8EALUB3VCUj31PJAACHZ0AP8CMP7MBABAWAA1U8nIAI3c3AXTYABvQDXGwTydP0gDyDAQBgB1fVADsHC8p1XQAQEBRJABtKzPLc9yADYAPctBPJ0gBWbzPIAZh80D3Kw7zPIADgc0D3KwmcCAGOdQF9FUVQKp9QLPYAYAATyAQtlL6UACJ0kBGz85oAAqQAyp1ALHstx2dVytPdDKstyzLsvq0CPO8nzquM0yzNAQoAF0yrSUjqsqqrXFqgBZNqopwTzPJALrXLsvrQB61rzMAwBWBx8iAABZMhyBYcgRwQBxFGAbBHAoABdAApAh6FwOAxEmvqgA) |
| 100 | +- [other answer](https://github.com/type-challenges/type-challenges/issues?q=label%3A612+label%3Aanswer) |
0 commit comments