Skip to content

Commit b11f017

Browse files
authoredNov 4, 2022
Add preserveConsecutiveUppercase option to CamelCase and add SplitWords type (#501)
1 parent 44cdf33 commit b11f017

9 files changed

+224
-38
lines changed
 

‎source/camel-case.d.ts

+29-24
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,42 @@
1-
import type {WordSeparators} from '../source/internal';
2-
import type {Split} from './split';
1+
import type {SplitWords} from './split-words';
32

43
/**
5-
Step by step takes the first item in an array literal, formats it and adds it to a string literal, and then recursively appends the remainder.
4+
CamelCase options.
65
7-
Only to be used by `CamelCaseStringArray<>`.
8-
9-
@see CamelCaseStringArray
6+
@see {@link CamelCase}
107
*/
11-
type InnerCamelCaseStringArray<Parts extends readonly any[], PreviousPart> =
12-
Parts extends [`${infer FirstPart}`, ...infer RemainingParts]
13-
? FirstPart extends undefined
14-
? ''
15-
: FirstPart extends ''
16-
? InnerCamelCaseStringArray<RemainingParts, PreviousPart>
17-
: `${PreviousPart extends '' ? FirstPart : Capitalize<FirstPart>}${InnerCamelCaseStringArray<RemainingParts, FirstPart>}`
18-
: '';
19-
20-
/**
21-
Starts fusing the output of `Split<>`, an array literal of strings, into a camel-cased string literal.
8+
export type CamelCaseOptions = {
9+
/**
10+
Whether to preserved consecutive uppercase letter.
2211
23-
It's separate from `InnerCamelCaseStringArray<>` to keep a clean API outwards to the rest of the code.
12+
@default true
13+
*/
14+
preserveConsecutiveUppercase?: boolean;
15+
};
2416

25-
@see Split
17+
/**
18+
Convert an array of words to camel-case.
2619
*/
27-
type CamelCaseStringArray<Parts extends readonly string[]> =
28-
Parts extends [`${infer FirstPart}`, ...infer RemainingParts]
29-
? Uncapitalize<`${FirstPart}${InnerCamelCaseStringArray<RemainingParts, FirstPart>}`>
30-
: never;
20+
type CamelCaseFromArray<
21+
Words extends string[],
22+
Options extends CamelCaseOptions,
23+
OutputString extends string = '',
24+
> = Words extends [
25+
infer FirstWord extends string,
26+
...infer RemainingWords extends string[],
27+
]
28+
? Options['preserveConsecutiveUppercase'] extends true
29+
? `${Capitalize<FirstWord>}${CamelCaseFromArray<RemainingWords, Options>}`
30+
: `${Capitalize<Lowercase<FirstWord>>}${CamelCaseFromArray<RemainingWords, Options>}`
31+
: OutputString;
3132

3233
/**
3334
Convert a string literal to camel-case.
3435
3536
This can be useful when, for example, converting some kebab-cased command-line flags or a snake-cased database result.
3637
38+
By default, consecutive uppercase letter are preserved. See {@link CamelCaseOptions.preserveConsecutiveUppercase preserveConsecutiveUppercase} option to change this behaviour.
39+
3740
@example
3841
```
3942
import type {CamelCase} from 'type-fest';
@@ -70,4 +73,6 @@ const dbResult: CamelCasedProperties<RawOptions> = {
7073
@category Change case
7174
@category Template literal
7275
*/
73-
export type CamelCase<K> = K extends string ? CamelCaseStringArray<Split<K extends Uppercase<K> ? Lowercase<K> : K, WordSeparators>> : K;
76+
export type CamelCase<Type, Options extends CamelCaseOptions = {preserveConsecutiveUppercase: true}> = Type extends string
77+
? Uncapitalize<CamelCaseFromArray<SplitWords<Type extends Uppercase<Type> ? Lowercase<Type> : Type>, Options>>
78+
: Type;

‎source/camel-cased-properties-deep.d.ts

+5-5
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {CamelCase} from './camel-case';
1+
import type {CamelCase, CamelCaseOptions} from './camel-case';
22

33
/**
44
Convert object properties to camel case recursively.
@@ -44,11 +44,11 @@ const result: CamelCasedPropertiesDeep<UserWithFriends> = {
4444
@category Template literal
4545
@category Object
4646
*/
47-
export type CamelCasedPropertiesDeep<Value> = Value extends Function
47+
export type CamelCasedPropertiesDeep<Value, Options extends CamelCaseOptions = {preserveConsecutiveUppercase: true}> = Value extends Function
4848
? Value
4949
: Value extends Array<infer U>
50-
? Array<CamelCasedPropertiesDeep<U>>
50+
? Array<CamelCasedPropertiesDeep<U, Options>>
5151
: Value extends Set<infer U>
52-
? Set<CamelCasedPropertiesDeep<U>> : {
53-
[K in keyof Value as CamelCase<K>]: CamelCasedPropertiesDeep<Value[K]>;
52+
? Set<CamelCasedPropertiesDeep<U, Options>> : {
53+
[K in keyof Value as CamelCase<K, Options>]: CamelCasedPropertiesDeep<Value[K], Options>;
5454
};

‎source/camel-cased-properties.d.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type {CamelCase} from './camel-case';
1+
import type {CamelCase, CamelCaseOptions} from './camel-case';
22

33
/**
44
Convert object properties to camel case but not recursively.
@@ -27,10 +27,10 @@ const result: CamelCasedProperties<User> = {
2727
@category Template literal
2828
@category Object
2929
*/
30-
export type CamelCasedProperties<Value> = Value extends Function
30+
export type CamelCasedProperties<Value, Options extends CamelCaseOptions = {preserveConsecutiveUppercase: true}> = Value extends Function
3131
? Value
3232
: Value extends Array<infer U>
3333
? Value
3434
: {
35-
[K in keyof Value as CamelCase<K>]: Value[K];
35+
[K in keyof Value as CamelCase<K, Options>]: Value[K];
3636
};

‎source/internal.d.ts

+15
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,18 @@ export type FirstArrayElement<TArray extends UnknownArrayOrTuple> = TArray exten
9393
Extracts the type of an array or tuple minus the first element.
9494
*/
9595
export type ArrayTail<TArray extends UnknownArrayOrTuple> = TArray extends readonly [unknown, ...infer TTail] ? TTail : [];
96+
97+
/**
98+
Returns a boolean for whether the string is lowercased.
99+
*/
100+
export type IsLowerCase<T extends string> = T extends Lowercase<T> ? true : false;
101+
102+
/**
103+
Returns a boolean for whether the string is uppercased.
104+
*/
105+
export type IsUpperCase<T extends string> = T extends Uppercase<T> ? true : false;
106+
107+
/**
108+
Returns a boolean for whether the string is numeric.
109+
*/
110+
export type IsNumeric<T extends string> = T extends `${number}` ? true : false;

‎source/split-words.d.ts

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type {IsLowerCase, IsNumeric, IsUpperCase, WordSeparators} from './internal';
2+
3+
type SkipEmptyWord<Word extends string> = Word extends '' ? [] : [Word];
4+
5+
type RemoveLastCharacter<Sentence extends string, Character extends string> = Sentence extends `${infer LeftSide}${Character}`
6+
? SkipEmptyWord<LeftSide>
7+
: never;
8+
9+
/**
10+
Split a string (almost) like Lodash's `_.words()` function.
11+
12+
- Split on each word that begins with a capital letter.
13+
- Split on each {@link WordSeparators}.
14+
- Split on numeric sequence.
15+
16+
@example
17+
```
18+
type Words0 = SplitWords<'helloWorld'>; // ['hello', 'World']
19+
type Words1 = SplitWords<'helloWORLD'>; // ['hello', 'WORLD']
20+
type Words2 = SplitWords<'hello-world'>; // ['hello', 'world']
21+
type Words3 = SplitWords<'--hello the_world'>; // ['hello', 'the', 'world']
22+
type Words4 = SplitWords<'lifeIs42'>; // ['life', 'Is', '42']
23+
```
24+
25+
@internal
26+
@category Change case
27+
@category Template literal
28+
*/
29+
export type SplitWords<
30+
Sentence extends string,
31+
LastCharacter extends string = '',
32+
CurrentWord extends string = '',
33+
> = Sentence extends `${infer FirstCharacter}${infer RemainingCharacters}`
34+
? FirstCharacter extends WordSeparators
35+
// Skip word separator
36+
? [...SkipEmptyWord<CurrentWord>, ...SplitWords<RemainingCharacters, LastCharacter>]
37+
: LastCharacter extends ''
38+
// Fist char of word
39+
? SplitWords<RemainingCharacters, FirstCharacter, FirstCharacter>
40+
// Case change: non-numeric to numeric, push word
41+
: [false, true] extends [IsNumeric<LastCharacter>, IsNumeric<FirstCharacter>]
42+
? [...SkipEmptyWord<CurrentWord>, ...SplitWords<RemainingCharacters, FirstCharacter, FirstCharacter>]
43+
// Case change: numeric to non-numeric, push word
44+
: [true, false] extends [IsNumeric<LastCharacter>, IsNumeric<FirstCharacter>]
45+
? [...SkipEmptyWord<CurrentWord>, ...SplitWords<RemainingCharacters, FirstCharacter, FirstCharacter>]
46+
// No case change: concat word
47+
: [true, true] extends [IsNumeric<LastCharacter>, IsNumeric<FirstCharacter>]
48+
? SplitWords<RemainingCharacters, FirstCharacter, `${CurrentWord}${FirstCharacter}`>
49+
// Case change: lower to upper, push word
50+
: [true, true] extends [IsLowerCase<LastCharacter>, IsUpperCase<FirstCharacter>]
51+
? [...SkipEmptyWord<CurrentWord>, ...SplitWords<RemainingCharacters, FirstCharacter, FirstCharacter>]
52+
// Case change: upper to lower, brings back the last character, push word
53+
: [true, true] extends [IsUpperCase<LastCharacter>, IsLowerCase<FirstCharacter>]
54+
? [...RemoveLastCharacter<CurrentWord, LastCharacter>, ...SplitWords<RemainingCharacters, FirstCharacter, `${LastCharacter}${FirstCharacter}`>]
55+
// No case change: concat word
56+
: SplitWords<RemainingCharacters, FirstCharacter, `${CurrentWord}${FirstCharacter}`>
57+
: [...SkipEmptyWord<CurrentWord>];

‎test-d/camel-case.ts

+10-5
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
import {expectType, expectAssignable} from 'tsd';
2-
import type {CamelCase, Split} from '../index';
3-
4-
// Split
5-
const prefixSplit: Split<'--very-prefixed', '-'> = ['', '', 'very', 'prefixed'];
6-
expectType<['', '', 'very', 'prefixed']>(prefixSplit);
2+
import type {CamelCase} from '../index';
73

84
// CamelCase
95
const camelFromPascal: CamelCase<'FooBar'> = 'fooBar';
@@ -70,3 +66,12 @@ expectAssignable<CamelCasedProperties<RawOptions>>({
7066
quzQux: 6,
7167
otherField: false,
7268
});
69+
70+
expectType<CamelCase<'fooBAR'>>('fooBAR');
71+
expectType<CamelCase<'fooBAR', {preserveConsecutiveUppercase: false}>>('fooBar');
72+
73+
expectType<CamelCase<'fooBARBiz'>>('fooBARBiz');
74+
expectType<CamelCase<'fooBARBiz', {preserveConsecutiveUppercase: false}>>('fooBarBiz');
75+
76+
expectType<CamelCase<'foo BAR-Biz_BUZZ'>>('fooBARBizBUZZ');
77+
expectType<CamelCase<'foo BAR-Biz_BUZZ', {preserveConsecutiveUppercase: false}>>('fooBarBizBuzz');

‎test-d/camel-cased-properties-deep.ts

+8
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ expectType<() => {a: string}>(fooBar);
1111
declare const bar: CamelCasedPropertiesDeep<Set<{fooBar: string}>>;
1212
expectType<Set<{fooBar: string}>>(bar);
1313

14+
type bazBizDeep = {fooBAR: number; baz: {fooBAR: number; BARFoo: string}};
15+
16+
declare const baz: CamelCasedPropertiesDeep<bazBizDeep>;
17+
expectType<{fooBAR: number; baz: {fooBAR: number; bARFoo: string}}>(baz);
18+
19+
declare const biz: CamelCasedPropertiesDeep<bazBizDeep, {preserveConsecutiveUppercase: false}>;
20+
expectType<{fooBar: number; baz: {fooBar: number; barFoo: string}}>(biz);
21+
1422
// Verify example
1523
type User = {
1624
UserId: number;

‎test-d/camel-cased-properties.ts

+6-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {expectType} from 'tsd';
22
import type {CamelCasedProperties} from '../index';
33

44
declare const foo: CamelCasedProperties<{A: number; B: {C: string}}>;
5-
65
expectType<{a: number; b: {C: string}}>(foo);
76

87
declare const bar: CamelCasedProperties<Array<{helloWorld: string}>>;
@@ -11,6 +10,12 @@ expectType<Array<{helloWorld: string}>>(bar);
1110
declare const fooBar: CamelCasedProperties<() => {a: string}>;
1211
expectType<() => {a: string}>(fooBar);
1312

13+
declare const baz: CamelCasedProperties<{fooBAR: number; BARFoo: string}>;
14+
expectType<{fooBAR: number; bARFoo: string}>(baz);
15+
16+
declare const biz: CamelCasedProperties<{fooBAR: number; BARFoo: string}, {preserveConsecutiveUppercase: false}>;
17+
expectType<{fooBar: number; barFoo: string}>(biz);
18+
1419
// Verify example
1520
type User = {
1621
UserId: number;

‎test-d/split-words.ts

+91
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import {expectType} from 'tsd';
2+
import type {SplitWords} from '../source/split-words';
3+
4+
expectType<SplitWords<''>>([]);
5+
expectType<SplitWords<'a'>>(['a']);
6+
expectType<SplitWords<'B'>>(['B']);
7+
expectType<SplitWords<'aa'>>(['aa']);
8+
expectType<SplitWords<'aB'>>(['a', 'B']);
9+
expectType<SplitWords<'Ba'>>(['Ba']);
10+
expectType<SplitWords<'BB'>>(['BB']);
11+
expectType<SplitWords<'aaa'>>(['aaa']);
12+
expectType<SplitWords<'aaB'>>(['aa', 'B']);
13+
expectType<SplitWords<'aBa'>>(['a', 'Ba']);
14+
expectType<SplitWords<'aBB'>>(['a', 'BB']);
15+
expectType<SplitWords<'Baa'>>(['Baa']);
16+
expectType<SplitWords<'BaB'>>(['Ba', 'B']);
17+
expectType<SplitWords<'BBa'>>(['B', 'Ba']);
18+
expectType<SplitWords<'BBB'>>(['BBB']);
19+
expectType<SplitWords<'aaaa'>>(['aaaa']);
20+
expectType<SplitWords<'aaaB'>>(['aaa', 'B']);
21+
expectType<SplitWords<'aaBa'>>(['aa', 'Ba']);
22+
expectType<SplitWords<'aaBB'>>(['aa', 'BB']);
23+
expectType<SplitWords<'aBaa'>>(['a', 'Baa']);
24+
expectType<SplitWords<'aBaB'>>(['a', 'Ba', 'B']);
25+
expectType<SplitWords<'aBBa'>>(['a', 'B', 'Ba']);
26+
expectType<SplitWords<'aBBB'>>(['a', 'BBB']);
27+
expectType<SplitWords<'Baaa'>>(['Baaa']);
28+
expectType<SplitWords<'BaaB'>>(['Baa', 'B']);
29+
expectType<SplitWords<'BaBa'>>(['Ba', 'Ba']);
30+
expectType<SplitWords<'BaBB'>>(['Ba', 'BB']);
31+
expectType<SplitWords<'BBaa'>>(['B', 'Baa']);
32+
expectType<SplitWords<'BBaB'>>(['B', 'Ba', 'B']);
33+
expectType<SplitWords<'BBBa'>>(['BB', 'Ba']);
34+
expectType<SplitWords<'BBBB'>>(['BBBB']);
35+
expectType<SplitWords<'aaaaa'>>(['aaaaa']);
36+
expectType<SplitWords<'aaaaB'>>(['aaaa', 'B']);
37+
expectType<SplitWords<'aaaBa'>>(['aaa', 'Ba']);
38+
expectType<SplitWords<'aaaBB'>>(['aaa', 'BB']);
39+
expectType<SplitWords<'aaBaa'>>(['aa', 'Baa']);
40+
expectType<SplitWords<'aaBaB'>>(['aa', 'Ba', 'B']);
41+
expectType<SplitWords<'aaBBa'>>(['aa', 'B', 'Ba']);
42+
expectType<SplitWords<'aaBBB'>>(['aa', 'BBB']);
43+
expectType<SplitWords<'aBaaa'>>(['a', 'Baaa']);
44+
expectType<SplitWords<'aBaaB'>>(['a', 'Baa', 'B']);
45+
expectType<SplitWords<'aBaBa'>>(['a', 'Ba', 'Ba']);
46+
expectType<SplitWords<'aBaBB'>>(['a', 'Ba', 'BB']);
47+
expectType<SplitWords<'aBBaa'>>(['a', 'B', 'Baa']);
48+
expectType<SplitWords<'aBBaB'>>(['a', 'B', 'Ba', 'B']);
49+
expectType<SplitWords<'aBBBa'>>(['a', 'BB', 'Ba']);
50+
expectType<SplitWords<'aBBBB'>>(['a', 'BBBB']);
51+
expectType<SplitWords<'Baaaa'>>(['Baaaa']);
52+
expectType<SplitWords<'BaaaB'>>(['Baaa', 'B']);
53+
expectType<SplitWords<'BaaBa'>>(['Baa', 'Ba']);
54+
expectType<SplitWords<'BaaBB'>>(['Baa', 'BB']);
55+
expectType<SplitWords<'BaBaa'>>(['Ba', 'Baa']);
56+
expectType<SplitWords<'BaBaB'>>(['Ba', 'Ba', 'B']);
57+
expectType<SplitWords<'BaBBa'>>(['Ba', 'B', 'Ba']);
58+
expectType<SplitWords<'BaBBB'>>(['Ba', 'BBB']);
59+
expectType<SplitWords<'BBaaa'>>(['B', 'Baaa']);
60+
expectType<SplitWords<'BBaaB'>>(['B', 'Baa', 'B']);
61+
expectType<SplitWords<'BBaBa'>>(['B', 'Ba', 'Ba']);
62+
expectType<SplitWords<'BBaBB'>>(['B', 'Ba', 'BB']);
63+
expectType<SplitWords<'BBBaa'>>(['BB', 'Baa']);
64+
expectType<SplitWords<'BBBaB'>>(['BB', 'Ba', 'B']);
65+
expectType<SplitWords<'BBBBa'>>(['BBB', 'Ba']);
66+
expectType<SplitWords<'BBBBB'>>(['BBBBB']);
67+
68+
expectType<SplitWords<'hello world'>>(['hello', 'world']);
69+
expectType<SplitWords<'Hello-world'>>(['Hello', 'world']);
70+
expectType<SplitWords<'hello_world'>>(['hello', 'world']);
71+
expectType<SplitWords<'hello world'>>(['hello', 'world']);
72+
expectType<SplitWords<'Hello--world'>>(['Hello', 'world']);
73+
expectType<SplitWords<'hello__world'>>(['hello', 'world']);
74+
expectType<SplitWords<' hello world'>>(['hello', 'world']);
75+
expectType<SplitWords<'---Hello--world'>>(['Hello', 'world']);
76+
expectType<SplitWords<'___hello__world'>>(['hello', 'world']);
77+
expectType<SplitWords<'hello world '>>(['hello', 'world']);
78+
expectType<SplitWords<'Hello--world--'>>(['Hello', 'world']);
79+
expectType<SplitWords<'hello__world___'>>(['hello', 'world']);
80+
expectType<SplitWords<'___ hello -__ _world'>>(['hello', 'world']);
81+
expectType<SplitWords<'__HelloWorld-HELLOWorld helloWORLD'>>(['Hello', 'World', 'HELLO', 'World', 'hello', 'WORLD']);
82+
83+
expectType<SplitWords<'item0'>>(['item', '0']);
84+
expectType<SplitWords<'item01'>>(['item', '01']);
85+
expectType<SplitWords<'item10'>>(['item', '10']);
86+
expectType<SplitWords<'item010'>>(['item', '010']);
87+
expectType<SplitWords<'0item0'>>(['0', 'item', '0']);
88+
expectType<SplitWords<'01item01'>>(['01', 'item', '01']);
89+
expectType<SplitWords<'10item10'>>(['10', 'item', '10']);
90+
expectType<SplitWords<'010item010'>>(['010', 'item', '010']);
91+
expectType<SplitWords<'item0_item_1 item -2'>>(['item', '0', 'item', '1', 'item', '2']);

0 commit comments

Comments
 (0)
Please sign in to comment.