Skip to content

Commit a463c30

Browse files
filipw01som-sm
andauthored
Improve DelimiterCase (#930)
Co-authored-by: Som Shekhar Mukherjee <iamssmkhrj@gmail.com>
1 parent db3403a commit a463c30

File tree

9 files changed

+466
-122
lines changed

9 files changed

+466
-122
lines changed

source/delimiter-case.d.ts

Lines changed: 31 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,23 @@
1-
import type {UpperCaseCharacters, WordSeparators} from './internal';
2-
3-
// Transforms a string that is fully uppercase into a fully lowercase version. Needed to add support for SCREAMING_SNAKE_CASE, see https://github.com/sindresorhus/type-fest/issues/385
4-
type UpperCaseToLowerCase<T extends string> = T extends Uppercase<T> ? Lowercase<T> : T;
5-
6-
// This implementation does not support SCREAMING_SNAKE_CASE, it is used internally by `SplitIncludingDelimiters`.
7-
type SplitIncludingDelimiters_<Source extends string, Delimiter extends string> =
8-
Source extends '' ? [] :
9-
Source extends `${infer FirstPart}${Delimiter}${infer SecondPart}` ?
10-
(
11-
Source extends `${FirstPart}${infer UsedDelimiter}${SecondPart}`
12-
? UsedDelimiter extends Delimiter
13-
? Source extends `${infer FirstPart}${UsedDelimiter}${infer SecondPart}`
14-
? [...SplitIncludingDelimiters<FirstPart, Delimiter>, UsedDelimiter, ...SplitIncludingDelimiters<SecondPart, Delimiter>]
15-
: never
16-
: never
17-
: never
18-
) :
19-
[Source];
20-
21-
/**
22-
Unlike a simpler split, this one includes the delimiter splitted on in the resulting array literal. This is to enable splitting on, for example, upper-case characters.
23-
24-
@category Template literal
25-
*/
26-
export type SplitIncludingDelimiters<Source extends string, Delimiter extends string> = SplitIncludingDelimiters_<UpperCaseToLowerCase<Source>, Delimiter>;
1+
import type {IsStringLiteral} from './is-literal';
2+
import type {Words, WordsOptions} from './words';
273

284
/**
29-
Format a specific part of the splitted string literal that `StringArrayToDelimiterCase<>` fuses together, ensuring desired casing.
30-
31-
@see StringArrayToDelimiterCase
5+
Convert an array of words to delimiter case starting with a delimiter with input capitalization.
326
*/
33-
type StringPartToDelimiterCase<StringPart extends string, Start extends boolean, UsedWordSeparators extends string, UsedUpperCaseCharacters extends string, Delimiter extends string> =
34-
StringPart extends UsedWordSeparators ? Delimiter :
35-
Start extends true ? Lowercase<StringPart> :
36-
StringPart extends UsedUpperCaseCharacters ? `${Delimiter}${Lowercase<StringPart>}` :
37-
StringPart;
38-
39-
/**
40-
Takes the result of a splitted string literal and recursively concatenates it together into the desired casing.
41-
42-
It receives `UsedWordSeparators` and `UsedUpperCaseCharacters` as input to ensure it's fully encapsulated.
43-
44-
@see SplitIncludingDelimiters
45-
*/
46-
type StringArrayToDelimiterCase<Parts extends readonly any[], Start extends boolean, UsedWordSeparators extends string, UsedUpperCaseCharacters extends string, Delimiter extends string> =
47-
Parts extends [`${infer FirstPart}`, ...infer RemainingParts]
48-
? `${StringPartToDelimiterCase<FirstPart, Start, UsedWordSeparators, UsedUpperCaseCharacters, Delimiter>}${StringArrayToDelimiterCase<RemainingParts, false, UsedWordSeparators, UsedUpperCaseCharacters, Delimiter>}`
49-
: Parts extends [string]
50-
? string
51-
: '';
7+
type DelimiterCaseFromArray<
8+
Words extends string[],
9+
Delimiter extends string,
10+
OutputString extends string = '',
11+
> = Words extends [
12+
infer FirstWord extends string,
13+
...infer RemainingWords extends string[],
14+
]
15+
? DelimiterCaseFromArray<RemainingWords, Delimiter, `${OutputString}${Delimiter}${FirstWord}`>
16+
: OutputString;
17+
18+
type RemoveFirstLetter<S extends string> = S extends `${infer _}${infer Rest}`
19+
? Rest
20+
: '';
5221

5322
/**
5423
Convert a string literal to a custom string delimiter casing.
@@ -65,6 +34,7 @@ import type {DelimiterCase} from 'type-fest';
6534
// Simple
6635
6736
const someVariable: DelimiterCase<'fooBar', '#'> = 'foo#bar';
37+
const someVariableNoSplitOnNumbers: DelimiterCase<'p2pNetwork', '#', {splitOnNumbers: false}> = 'p2p#network';
6838
6939
// Advanced
7040
@@ -87,13 +57,17 @@ const rawCliOptions: OddlyCasedProperties<SomeOptions> = {
8757
8858
@category Change case
8959
@category Template literal
90-
*/
91-
export type DelimiterCase<Value, Delimiter extends string> = string extends Value ? Value : Value extends string
92-
? StringArrayToDelimiterCase<
93-
SplitIncludingDelimiters<Value, WordSeparators | UpperCaseCharacters>,
94-
true,
95-
WordSeparators,
96-
UpperCaseCharacters,
97-
Delimiter
98-
>
60+
*/
61+
export type DelimiterCase<
62+
Value,
63+
Delimiter extends string,
64+
Options extends WordsOptions = {},
65+
> = Value extends string
66+
? IsStringLiteral<Value> extends false
67+
? Value
68+
: Lowercase<
69+
RemoveFirstLetter<
70+
DelimiterCaseFromArray<Words<Value, Options>, Delimiter>
71+
>
72+
>
9973
: Value;

source/kebab-case.d.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type {DelimiterCase} from './delimiter-case';
2+
import type {WordsOptions} from './words';
23

34
/**
45
Convert a string literal to kebab-case.
@@ -12,6 +13,7 @@ import type {KebabCase} from 'type-fest';
1213
// Simple
1314
1415
const someVariable: KebabCase<'fooBar'> = 'foo-bar';
16+
const someVariableNoSplitOnNumbers: KebabCase<'p2pNetwork', {splitOnNumbers: false}> = 'p2p-network';
1517
1618
// Advanced
1719
@@ -35,4 +37,7 @@ const rawCliOptions: KebabCasedProperties<CliOptions> = {
3537
@category Change case
3638
@category Template literal
3739
*/
38-
export type KebabCase<Value> = DelimiterCase<Value, '-'>;
40+
export type KebabCase<
41+
Value,
42+
Options extends WordsOptions = {},
43+
> = DelimiterCase<Value, '-', Options>;

source/screaming-snake-case.d.ts

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,5 @@
1-
import type {SplitIncludingDelimiters} from './delimiter-case';
21
import type {SnakeCase} from './snake-case';
3-
import type {Includes} from './includes';
4-
5-
/**
6-
Returns a boolean for whether the string is screaming snake case.
7-
*/
8-
type IsScreamingSnakeCase<Value extends string> = Value extends Uppercase<Value>
9-
? Includes<SplitIncludingDelimiters<Lowercase<Value>, '_'>, '_'> extends true
10-
? true
11-
: false
12-
: false;
2+
import type {WordsOptions} from './words';
133

144
/**
155
Convert a string literal to screaming-snake-case.
@@ -21,13 +11,14 @@ This can be useful when, for example, converting a camel-cased object property t
2111
import type {ScreamingSnakeCase} from 'type-fest';
2212
2313
const someVariable: ScreamingSnakeCase<'fooBar'> = 'FOO_BAR';
14+
const someVariableNoSplitOnNumbers: ScreamingSnakeCase<'p2pNetwork', {splitOnNumbers: false}> = 'P2P_NETWORK';
15+
2416
```
2517
2618
@category Change case
2719
@category Template literal
28-
*/
29-
export type ScreamingSnakeCase<Value> = Value extends string
30-
? IsScreamingSnakeCase<Value> extends true
31-
? Value
32-
: Uppercase<SnakeCase<Value>>
33-
: Value;
20+
*/
21+
export type ScreamingSnakeCase<
22+
Value,
23+
Options extends WordsOptions = {},
24+
> = Value extends string ? Uppercase<SnakeCase<Value, Options>> : Value;

source/snake-case.d.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type {DelimiterCase} from './delimiter-case';
2+
import type {WordsOptions} from './words';
23

34
/**
45
Convert a string literal to snake-case.
@@ -12,6 +13,7 @@ import type {SnakeCase} from 'type-fest';
1213
// Simple
1314
1415
const someVariable: SnakeCase<'fooBar'> = 'foo_bar';
16+
const someVariableNoSplitOnNumbers: SnakeCase<'p2pNetwork', {splitOnNumbers: false}> = 'p2p_network';
1517
1618
// Advanced
1719
@@ -35,4 +37,7 @@ const dbResult: SnakeCasedProperties<ModelProps> = {
3537
@category Change case
3638
@category Template literal
3739
*/
38-
export type SnakeCase<Value> = DelimiterCase<Value, '_'>;
40+
export type SnakeCase<
41+
Value,
42+
Options extends WordsOptions = {},
43+
> = DelimiterCase<Value, '_', Options>;

source/words.d.ts

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,46 @@
1-
import type {IsLowerCase, IsNumeric, IsUpperCase, WordSeparators} from './internal';
1+
import type {
2+
IsLowerCase,
3+
IsNumeric,
4+
IsUpperCase,
5+
WordSeparators,
6+
} from './internal';
27

38
type SkipEmptyWord<Word extends string> = Word extends '' ? [] : [Word];
49

5-
type RemoveLastCharacter<Sentence extends string, Character extends string> = Sentence extends `${infer LeftSide}${Character}`
10+
type RemoveLastCharacter<
11+
Sentence extends string,
12+
Character extends string,
13+
> = Sentence extends `${infer LeftSide}${Character}`
614
? SkipEmptyWord<LeftSide>
715
: never;
816

17+
/**
18+
Words options.
19+
20+
@see {@link Words}
21+
*/
22+
export type WordsOptions = {
23+
/**
24+
Split on numeric sequence.
25+
26+
@default true
27+
28+
@example
29+
```
30+
type Example1 = Words<'p2pNetwork', {splitOnNumbers: true}>;
31+
//=> ["p", "2", "p", "Network"]
32+
33+
type Example2 = Words<'p2pNetwork', {splitOnNumbers: false}>;
34+
//=> ["p2p", "Network"]
35+
```
36+
*/
37+
splitOnNumbers?: boolean;
38+
};
39+
40+
type DefaultOptions = {
41+
splitOnNumbers: true;
42+
};
43+
944
/**
1045
Split a string (almost) like Lodash's `_.words()` function.
1146
@@ -31,37 +66,53 @@ type Words3 = Words<'--hello the_world'>;
3166
3267
type Words4 = Words<'lifeIs42'>;
3368
//=> ['life', 'Is', '42']
69+
70+
type Words5 = Words<'p2pNetwork', {splitOnNumbers: false}>;
71+
//=> ['p2p', 'Network']
3472
```
3573
3674
@category Change case
3775
@category Template literal
3876
*/
39-
export type Words<
77+
export type Words<Sentence extends string, Options extends WordsOptions = {}> = WordsImplementation<Sentence, {
78+
splitOnNumbers: Options['splitOnNumbers'] extends boolean ? Options['splitOnNumbers'] : DefaultOptions['splitOnNumbers'];
79+
}>;
80+
81+
type WordsImplementation<
4082
Sentence extends string,
83+
Options extends Required<WordsOptions>,
4184
LastCharacter extends string = '',
4285
CurrentWord extends string = '',
4386
> = Sentence extends `${infer FirstCharacter}${infer RemainingCharacters}`
4487
? FirstCharacter extends WordSeparators
4588
// Skip word separator
46-
? [...SkipEmptyWord<CurrentWord>, ...Words<RemainingCharacters>]
89+
? [...SkipEmptyWord<CurrentWord>, ...WordsImplementation<RemainingCharacters, Options>]
4790
: LastCharacter extends ''
4891
// Fist char of word
49-
? Words<RemainingCharacters, FirstCharacter, FirstCharacter>
50-
// Case change: non-numeric to numeric, push word
92+
? WordsImplementation<RemainingCharacters, Options, FirstCharacter, FirstCharacter>
93+
// Case change: non-numeric to numeric
5194
: [false, true] extends [IsNumeric<LastCharacter>, IsNumeric<FirstCharacter>]
52-
? [...SkipEmptyWord<CurrentWord>, ...Words<RemainingCharacters, FirstCharacter, FirstCharacter>]
53-
// Case change: numeric to non-numeric, push word
95+
? Options['splitOnNumbers'] extends true
96+
// Split on number: push word
97+
? [...SkipEmptyWord<CurrentWord>, ...WordsImplementation<RemainingCharacters, Options, FirstCharacter, FirstCharacter>]
98+
// No split on number: concat word
99+
: WordsImplementation<RemainingCharacters, Options, FirstCharacter, `${CurrentWord}${FirstCharacter}`>
100+
// Case change: numeric to non-numeric
54101
: [true, false] extends [IsNumeric<LastCharacter>, IsNumeric<FirstCharacter>]
55-
? [...SkipEmptyWord<CurrentWord>, ...Words<RemainingCharacters, FirstCharacter, FirstCharacter>]
102+
? Options['splitOnNumbers'] extends true
103+
// Split on number: push word
104+
? [...SkipEmptyWord<CurrentWord>, ...WordsImplementation<RemainingCharacters, Options, FirstCharacter, FirstCharacter>]
105+
// No split on number: concat word
106+
: WordsImplementation<RemainingCharacters, Options, FirstCharacter, `${CurrentWord}${FirstCharacter}`>
56107
// No case change: concat word
57108
: [true, true] extends [IsNumeric<LastCharacter>, IsNumeric<FirstCharacter>]
58-
? Words<RemainingCharacters, FirstCharacter, `${CurrentWord}${FirstCharacter}`>
109+
? WordsImplementation<RemainingCharacters, Options, FirstCharacter, `${CurrentWord}${FirstCharacter}`>
59110
// Case change: lower to upper, push word
60111
: [true, true] extends [IsLowerCase<LastCharacter>, IsUpperCase<FirstCharacter>]
61-
? [...SkipEmptyWord<CurrentWord>, ...Words<RemainingCharacters, FirstCharacter, FirstCharacter>]
112+
? [...SkipEmptyWord<CurrentWord>, ...WordsImplementation<RemainingCharacters, Options, FirstCharacter, FirstCharacter>]
62113
// Case change: upper to lower, brings back the last character, push word
63114
: [true, true] extends [IsUpperCase<LastCharacter>, IsLowerCase<FirstCharacter>]
64-
? [...RemoveLastCharacter<CurrentWord, LastCharacter>, ...Words<RemainingCharacters, FirstCharacter, `${LastCharacter}${FirstCharacter}`>]
115+
? [...RemoveLastCharacter<CurrentWord, LastCharacter>, ...WordsImplementation<RemainingCharacters, Options, FirstCharacter, `${LastCharacter}${FirstCharacter}`>]
65116
// No case change: concat word
66-
: Words<RemainingCharacters, FirstCharacter, `${CurrentWord}${FirstCharacter}`>
117+
: WordsImplementation<RemainingCharacters, Options, FirstCharacter, `${CurrentWord}${FirstCharacter}`>
67118
: [...SkipEmptyWord<CurrentWord>];

0 commit comments

Comments
 (0)