Skip to content

Commit 1c867f2

Browse files
committed
Add KebabCase<>
As dicussed in TypeScript 4.1 type ideas sindresorhus#134
1 parent 1c294c3 commit 1c867f2

File tree

3 files changed

+86
-0
lines changed

3 files changed

+86
-0
lines changed

index.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export {RequireAtLeastOne} from './source/require-at-least-one';
1010
export {RequireExactlyOne} from './source/require-exactly-one';
1111
export {PartialDeep} from './source/partial-deep';
1212
export {ReadonlyDeep} from './source/readonly-deep';
13+
export {KebabCase} from './source/kebab-case';
1314
export {LiteralUnion} from './source/literal-union';
1415
export {Promisable} from './source/promisable';
1516
export {Opaque} from './source/opaque';

source/kebab-case.d.ts

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
type SplitIncludingDelimitor<S extends string, D extends string> =
2+
string extends S ? string[] :
3+
S extends '' ? [] :
4+
S extends `${infer T}${D}${infer U}` ?
5+
( S extends `${T}${infer Z}${U}` ? [T, Z, ...SplitIncludingDelimitor<U, D>] : never ) :
6+
[S];
7+
8+
type Join<S extends any[], D extends string> =
9+
string[] extends S ? string :
10+
S extends [`${infer T}`, ...infer U] ?
11+
U[0] extends undefined ? T : `${T}${D}${Join<U, D>}` :
12+
'';
13+
14+
type UpperCaseChars = 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' | 'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'X' | 'Y' | 'Z';
15+
type WordSeparators = "-"|"_"|" ";
16+
17+
type StringPartToKebabCase<K extends string, S extends string, L extends string> =
18+
K extends S ? '-' :
19+
K extends L ? `-${Lowercase<K>}` :
20+
K;
21+
type StringArrayToKebabCase<K extends any[], S extends string, L extends string> =
22+
K extends [`${infer T}`, ...infer U] ? `${StringPartToKebabCase<T, S, L>}${StringArrayToKebabCase<U, S, L>}` :
23+
'';
24+
25+
/**
26+
Converts a string literal that may use another casing than kebab casing to kebab casing
27+
28+
This can be useful when eg. converting a camel cased object property to eg. a CSS class name or a CLI option.
29+
30+
@example
31+
```
32+
import {KebabCase} from 'type-fest';
33+
34+
type KebabCasedProps<T> = {
35+
[K in keyof T as KebabCase<K>]: T[K]
36+
};
37+
38+
interface CliOptions {
39+
dryRun: boolean;
40+
includeFile: string;
41+
foo: number;
42+
}
43+
44+
const rawCliOptions: KebabCasedProps<CliOptions> = {
45+
'dry-run': true,
46+
'include-file': 'bar.js',
47+
foo: 123
48+
};
49+
```
50+
*/
51+
export type KebabCase<K> = K extends string ? StringArrayToKebabCase<SplitIncludingDelimitor<K, WordSeparators | UpperCaseChars>, WordSeparators, UpperCaseChars> : K;

test-d/kebab-case.ts

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import {KebabCase} from '../source/kebab-case';
2+
import {expectType, expectAssignable} from 'tsd';
3+
4+
const kebabFromCamel: KebabCase<'fooBar'> = 'foo-bar';
5+
expectType<'foo-bar'>(kebabFromCamel);
6+
7+
const kebabFromKebab: KebabCase<'foo-bar'> = 'foo-bar';
8+
expectType<'foo-bar'>(kebabFromKebab);
9+
10+
const kebabFromSpace: KebabCase<'foo bar'> = 'foo-bar';
11+
expectType<'foo-bar'>(kebabFromSpace);
12+
13+
const kebabFromSnake: KebabCase<'foo_bar'> = 'foo-bar';
14+
expectType<'foo-bar'>(kebabFromSnake);
15+
16+
const noKebabFromMono: KebabCase<'foobar'> = 'foobar';
17+
expectType<'foobar'>(noKebabFromMono);
18+
19+
// Verifying example
20+
type KebabCasedProps<T> = {
21+
[K in keyof T as KebabCase<K>]: T[K]
22+
};
23+
24+
interface CliOptions {
25+
dryRun: boolean;
26+
includeFile: string;
27+
foo: number;
28+
}
29+
30+
expectAssignable<KebabCasedProps<CliOptions>>({
31+
'dry-run': true,
32+
'include-file': 'bar.js',
33+
foo: 123
34+
});

0 commit comments

Comments
 (0)