Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
87f5129
fix/1229: add splitOnPunctuation word option. Add minimal tests.
jericirenej Mar 27, 2026
3ca5e8f
fix/1229: use 1-based numbering in Words docs
jericirenej Mar 29, 2026
269d628
fix/1229: CamelCase: Merge default Word options. Add tests for splitO…
jericirenej Mar 29, 2026
2422fda
fix/1229: KebabCase: add tests for splitOnPunctuation. Update JSDoc.
jericirenej Mar 29, 2026
bd60cd3
fix/1229: PascalCase: add tests for splitOnPunctuation. Update JSDoc.
jericirenej Mar 29, 2026
159cba1
fix/1229: SnakeCase: add tests for splitOnPunctuation. Update JSDoc.
jericirenej Mar 29, 2026
7f6685b
fix/1229: lint fixes
jericirenej Mar 29, 2026
ba095c3
fix/1229: remove unneeded examples from kebab and snake cases.
jericirenej Mar 29, 2026
9fb092e
fix/1229: since splintOnPunctuation si false by default, first exampl…
jericirenej Mar 29, 2026
0a2d17f
fix/1229: formatting
jericirenej Mar 29, 2026
f63d513
fix/1229: remove unneeded comment in test file
jericirenej Mar 29, 2026
424a798
doc: remove leading space from JSDoc
som-sm Mar 29, 2026
e0120b9
doc: wrap JSDoc codeblock in fences
som-sm Mar 29, 2026
346be78
doc: improve JSDoc description
som-sm Mar 29, 2026
6749084
doc: improve JSDoc codeblocks
som-sm Mar 29, 2026
037c4fd
fix/1229: JSDOcs reverted to pre-existing pattern. New feature docume…
jericirenej Mar 29, 2026
3f730e9
fix/1229: Add tests with splitOnPunctuation and splitOnNumbers
jericirenej Mar 29, 2026
aa9ddee
fix/1229: lint fix
jericirenej Mar 29, 2026
e93c9ef
fix/1229: new tests follow old pattern.
jericirenej Mar 29, 2026
149564f
fix/1229: snake-case: fix unused test constant. Additional test for p…
jericirenej Mar 29, 2026
668146f
fix/1229: add tests for x-cased-properties and x-cased-properties-deep
jericirenej Mar 29, 2026
d65a39d
fix/1229: CamelCasedPropertiesDeep: add missing user role type to pun…
jericirenej Mar 29, 2026
6467ed2
fix/1229: update x-cased-properties-y JSDocs.
jericirenej Mar 29, 2026
7ae0189
fix/1292: remove unnecessary changes
jericirenej Mar 29, 2026
d8df5a1
Update pascal-case.d.ts
sindresorhus Mar 29, 2026
ba452c4
Update kebab-case.ts
sindresorhus Mar 29, 2026
197324f
Update pascal-case.ts
sindresorhus Mar 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions source/camel-case.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {ApplyDefaultOptions} from './internal/index.d.ts';
import type {Words, WordsOptions} from './words.d.ts';
import type {_DefaultWordsOptions, Words, WordsOptions} from './words.d.ts';

/**
CamelCase options.
Expand All @@ -15,8 +15,7 @@ export type CamelCaseOptions = WordsOptions & {
preserveConsecutiveUppercase?: boolean;
};

export type _DefaultCamelCaseOptions = {
splitOnNumbers: true;
export type _DefaultCamelCaseOptions = _DefaultWordsOptions & {
preserveConsecutiveUppercase: false;
};

Expand Down Expand Up @@ -51,6 +50,7 @@ import type {CamelCase} from 'type-fest';

const someVariable: CamelCase<'foo-bar'> = 'fooBar';
const preserveConsecutiveUppercase: CamelCase<'foo-BAR-baz', {preserveConsecutiveUppercase: true}> = 'fooBARBaz';
const splitOnPunctuation: CamelCase<'foo-bar:BAZ', {splitOnPunctuation: true}> = 'fooBarBaz';

// Advanced

Expand Down
7 changes: 7 additions & 0 deletions source/camel-cased-properties-deep.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ const preserveConsecutiveUppercase: CamelCasedPropertiesDeep<{fooBAR: {fooBARBiz
}],
},
};

const splitOnPunctuation: CamelCasedPropertiesDeep<{'user@info': {'user::id': number; 'user::name': string}}, {splitOnPunctuation: true}> = {
userInfo: {
userId: 1,
userName: 'Tom',
},
};
```

@category Change case
Expand Down
4 changes: 4 additions & 0 deletions source/camel-cased-properties.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ const result: CamelCasedProperties<User> = {
const preserveConsecutiveUppercase: CamelCasedProperties<{fooBAR: string}, {preserveConsecutiveUppercase: true}> = {
fooBAR: 'string',
};
const splitOnPunctuation: CamelCasedProperties<{'foo::bar': string}, {splitOnPunctuation: true}> = {
fooBar: 'string',
};
```
@category Change case
Expand Down
1 change: 1 addition & 0 deletions source/kebab-case.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import type {KebabCase} from 'type-fest';
const someVariable: KebabCase<'fooBar'> = 'foo-bar';
const someVariableNoSplitOnNumbers: KebabCase<'p2pNetwork', {splitOnNumbers: false}> = 'p2p-network';
const someVariableWithPunctuation: KebabCase<'div.card::after', {splitOnPunctuation: true}> = 'div-card-after';
// Advanced
Expand Down
7 changes: 7 additions & 0 deletions source/kebab-cased-properties-deep.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ const splitOnNumbers: KebabCasedPropertiesDeep<{line1: {line2: [{line3: string}]
],
},
};
const splitOnPunctuation: KebabCasedPropertiesDeep<{'user@info': {'user::id': number; 'user::name': string}}, {splitOnPunctuation: true}> = {
'user-info': {
'user-id': 1,
'user-name': 'Tom',
},
};
```
@category Change case
Expand Down
4 changes: 4 additions & 0 deletions source/kebab-cased-properties.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ const result: KebabCasedProperties<User> = {
const splitOnNumbers: KebabCasedProperties<{line1: string}, {splitOnNumbers: true}> = {
'line-1': 'string',
};
const splitOnPunctuation: KebabCasedProperties<{'foo::bar': string}, {splitOnPunctuation: true}> = {
'foo-bar': 'string',
};
```
@category Change case
Expand Down
1 change: 1 addition & 0 deletions source/pascal-case.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {PascalCase} from 'type-fest';
const someVariable: PascalCase<'foo-bar'> = 'FooBar';
const preserveConsecutiveUppercase: PascalCase<'foo-BAR-baz', {preserveConsecutiveUppercase: true}> = 'FooBARBaz';
const splitOnPunctuation: PascalCase<'foo-bar>>baz', {splitOnPunctuation: true}> = 'FooBarBaz';
// Advanced
Expand Down
7 changes: 7 additions & 0 deletions source/pascal-cased-properties-deep.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ const preserveConsecutiveUppercase: PascalCasedPropertiesDeep<{fooBAR: {fooBARBi
}],
},
};
const splitOnPunctuation: PascalCasedPropertiesDeep<{'user@info': {'user::id': number; 'user::name': string}}, {splitOnPunctuation: true}> = {
UserInfo: {
UserId: 1,
UserName: 'Tom',
},
};
```
@category Change case
Expand Down
4 changes: 4 additions & 0 deletions source/pascal-cased-properties.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ const result: PascalCasedProperties<User> = {
const preserveConsecutiveUppercase: PascalCasedProperties<{fooBAR: string}, {preserveConsecutiveUppercase: true}> = {
FooBAR: 'string',
};
const splitOnPunctuation: PascalCasedProperties<{'foo::bar': string}, {splitOnPunctuation: true}> = {
FooBar: 'string',
};
```
@category Change case
Expand Down
1 change: 1 addition & 0 deletions source/snake-case.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {SnakeCase} from 'type-fest';
const someVariable: SnakeCase<'fooBar'> = 'foo_bar';
const noSplitOnNumbers: SnakeCase<'p2pNetwork'> = 'p2p_network';
const splitOnNumbers: SnakeCase<'p2pNetwork', {splitOnNumbers: true}> = 'p_2_p_network';
const splitOnPunctuation: SnakeCase<'div.card::after', {splitOnPunctuation: true}> = 'div_card_after';
// Advanced
Expand Down
7 changes: 7 additions & 0 deletions source/snake-cased-properties-deep.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ const splitOnNumbers: SnakeCasedPropertiesDeep<{line1: {line2: [{line3: string}]
],
},
};
const splitOnPunctuation: SnakeCasedPropertiesDeep<{'user@info': {'user::id': number; 'user::name': string}}, {splitOnPunctuation: true}> = {
'user_info': {
'user_id': 1,
'user_name': 'Tom',
},
};
```
@category Change case
Expand Down
4 changes: 4 additions & 0 deletions source/snake-cased-properties.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ const result: SnakeCasedProperties<User> = {
const splitOnNumbers: SnakeCasedProperties<{line1: string}, {splitOnNumbers: true}> = {
'line_1': 'string',
};
const splitOnPunctuation: SnakeCasedProperties<{'foo::bar': string}, {splitOnPunctuation: true}> = {
'foo_bar': 'string',
};
```
@category Change case
Expand Down
32 changes: 29 additions & 3 deletions source/words.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type {
ApplyDefaultOptions,
AsciiPunctuation,
IsNumeric,
WordSeparators,
} from './internal/index.d.ts';
Expand Down Expand Up @@ -38,17 +39,34 @@ export type WordsOptions = {
```
*/
splitOnNumbers?: boolean;
/**
Split on {@link AsciiPunctuation | punctuation characters} (e.g., `#`, `&`, `*`, `:`, `?`, `@`, `~`).

@example
```
import type {Words} from 'type-fest';

type Example1 = Words<'hello:world', {splitOnPunctuation: true}>;
//=> ['hello', 'world']

type Example2 = Words<'hello:world', {splitOnPunctuation: false}>;
//=> ['hello', ':world']
```
*/
splitOnPunctuation?: boolean;
};

export type _DefaultWordsOptions = {
splitOnNumbers: true;
splitOnPunctuation: false;
};

/**
Split a string (almost) like Lodash's `_.words()` function.

- Split on each word that begins with a capital letter.
- Split on each {@link WordSeparators}.
- Split on each {@link AsciiPunctuation} (if {@link WordsOptions.splitOnPunctuation} is enabled).
- Split on numeric sequence.

@example
Expand All @@ -72,21 +90,29 @@ type Words4 = Words<'lifeIs42'>;

type Words5 = Words<'p2pNetwork', {splitOnNumbers: false}>;
//=> ['p2p', 'Network']

type Words6 = Words<'hello:world', {splitOnPunctuation: true}>;
//=> ['hello', 'world']

type Words7 = Words<'hello:world', {splitOnPunctuation: false}>;
//=> ['hello', ':world']

type Words8 = Words<'hello:world', {splitOnPunctuation: true}>;
//=> ['hello', 'world']
```

@category Change case
@category Template literal
*/
export type Words<Sentence extends string, Options extends WordsOptions = {}> =
WordsImplementation<Sentence, ApplyDefaultOptions<WordsOptions, _DefaultWordsOptions, Options>>;
export type Words<Sentence extends string, Options extends WordsOptions = {}> = WordsImplementation<Sentence, ApplyDefaultOptions<WordsOptions, _DefaultWordsOptions, Options>>;

type WordsImplementation<
Sentence extends string,
Options extends Required<WordsOptions>,
LastCharacter extends string = '',
CurrentWord extends string = '',
> = Sentence extends `${infer FirstCharacter}${infer RemainingCharacters}`
? FirstCharacter extends WordSeparators
? FirstCharacter extends WordSeparators | (Options['splitOnPunctuation'] extends true ? AsciiPunctuation : never)
// Skip word separator
? [...SkipEmptyWord<CurrentWord>, ...WordsImplementation<RemainingCharacters, Options>]
: LastCharacter extends ''
Expand Down
12 changes: 12 additions & 0 deletions test-d/camel-case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,15 @@ expectType<'a1BText'>('' as CamelCase<'a1b_text'>);

expectType<'p2pNetwork'>('' as CamelCase<'p2pNetwork', {splitOnNumbers: false}>);
expectType<'p2PNetwork'>('' as CamelCase<'p2pNetwork', {splitOnNumbers: true}>);

// Punctuation
expectType<CamelCase<'onDialog:close'>>('onDialog:close');
expectType<CamelCase<'foo-bar>>baz'>>('fooBar>>baz');
expectType<CamelCase<'foo-bar::01'>>('fooBar::01');

expectType<CamelCase<'onDialog:close', {splitOnPunctuation: true}>>('onDialogClose');
expectType<CamelCase<'foo-bar>>baz', {splitOnPunctuation: true}>>('fooBarBaz');
expectType<CamelCase<'fooBAR:biz', {splitOnPunctuation: true; preserveConsecutiveUppercase: true}>>('fooBARBiz');
expectType<CamelCase<'foo-bar::01', {splitOnPunctuation: true}>>('fooBar01');
expectType<CamelCase<'foo-bar::01', {splitOnPunctuation: true; splitOnNumbers: false}>>('fooBar01');
expectType<CamelCase<'foo-bar::01', {splitOnPunctuation: true; splitOnNumbers: true}>>('fooBar01');
18 changes: 18 additions & 0 deletions test-d/camel-cased-properties-deep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,24 @@ type User = {
Role: UserRole;
};

type UserPunctuated = {
'user::id': number;
'user::name': string;
date: Date;
'reg::exp': RegExp;
Role: UserRole;
};

type UserWithFriends = {
UserInfo: User;
UserFriends: User[];
};

type UserWithFriendsPunctuated = {
'user@info': UserPunctuated;
'user#friends': UserPunctuated[];
};

const role = 'someRole' as UserRole;

const result: CamelCasedPropertiesDeep<UserWithFriends> = {
Expand Down Expand Up @@ -65,10 +78,15 @@ const result: CamelCasedPropertiesDeep<UserWithFriends> = {
},
],
};

expectType<CamelCasedPropertiesDeep<UserWithFriends>>(result);
expectType<CamelCasedPropertiesDeep<UserWithFriendsPunctuated, {splitOnPunctuation: true}>>(result);

expectType<{fooBar: unknown}>({} as CamelCasedPropertiesDeep<{foo_bar: unknown}>);
expectType<{fooBar: {barBaz: unknown}; biz: unknown}>({} as CamelCasedPropertiesDeep<{foo_bar: {bar_baz: unknown}; biz: unknown}>);

expectType<{fooBar: any}>({} as CamelCasedPropertiesDeep<{foo_bar: any}>);
expectType<{fooBar: {barBaz: any}; biz: any}>({} as CamelCasedPropertiesDeep<{foo_bar: {bar_baz: any}; biz: any}>);

expectType<{'fooBar': unknown}>({} as CamelCasedPropertiesDeep<{'foo::bar': unknown}, {splitOnPunctuation: true}>);
expectType<{'fooBar': {'barBaz': unknown}; biz: unknown}>({} as CamelCasedPropertiesDeep<{'foo::bar': {'bar@baz': unknown}; biz: unknown}, {splitOnPunctuation: true}>);
15 changes: 15 additions & 0 deletions test-d/camel-cased-properties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,29 @@ expectType<{fooBAR: number; bARFoo: string}>(baz);
declare const biz: CamelCasedProperties<{fooBAR: number; BARFoo: string}>;
expectType<{fooBar: number; barFoo: string}>(biz);

declare const fooBarPunctuated: CamelCasedProperties<{'hello@world1': {'foo::bar': string}}>;
expectType<{'hello@world1': {'foo::bar': string}}>(fooBarPunctuated);

declare const fooBarPunctuatedSplit: CamelCasedProperties<{'hello@world1': {'foo::bar': string}}, {splitOnPunctuation: true}>;
expectType<{'helloWorld1': {'foo::bar': string}}>(fooBarPunctuatedSplit);

declare const fooBarPunctuatedSplitNumberSplit: CamelCasedProperties<{'hello@world1': {'foo::bar': string}}, {splitOnPunctuation: true; splitOnNumbers: true}>;
expectType<{'helloWorld1': {'foo::bar': string}}>(fooBarPunctuatedSplitNumberSplit);

// Verify example
type User = {
UserId: number;
UserName: string;
};

type UserPunctuated = {
'user::id': number;
'user::name': string;
};

const result: CamelCasedProperties<User> = {
userId: 1,
userName: 'Tom',
};
expectType<CamelCasedProperties<User>>(result);
expectType<CamelCasedProperties<UserPunctuated, {splitOnPunctuation: true}>>(result);
26 changes: 24 additions & 2 deletions test-d/kebab-case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,20 +79,42 @@ expectType<'fo-o2bar'>(kebabFromNumberInTheMiddleNoSplitOnNumbersEdgeCase2);
const kebabFromNumberInTheMiddleNoSplitOnNumbersEdgeCase3: KebabCase<'FOO22Bar'> = 'foo22-bar';
expectType<'foo22-bar'>(kebabFromNumberInTheMiddleNoSplitOnNumbersEdgeCase3);

// Punctuation
declare const withPunctuation: KebabCase<'onDialog:close'>;
expectType<'on-dialog:close'>(withPunctuation);

declare const withPunctuationAndSplit: KebabCase<'onDialog:close', {splitOnPunctuation: true}>;
expectType<'on-dialog-close'>(withPunctuationAndSplit);

declare const withPunctuation2: KebabCase<'foo-bar>>baz'>;
expectType<'foo-bar>>baz'>(withPunctuation2);

declare const withPunctuationAndSplit2: KebabCase<'foo-bar>>baz', {splitOnPunctuation: true}>;
expectType<'foo-bar-baz'>(withPunctuationAndSplit2);

declare const withPunctuation3: KebabCase<'card::after'>;
expectType<'card::after'>(withPunctuation3);

declare const withPunctuationAndSplit3: KebabCase<'card::after', {splitOnPunctuation: true}>;
expectType<'card-after'>(withPunctuationAndSplit3);

declare const withPunctuation4: KebabCase<'div.card::after'>;
expectType<'div.card::after'>(withPunctuation4);

declare const withPunctuationAndSplit4: KebabCase<'div.card::after', {splitOnPunctuation: true}>;
expectType<'div-card-after'>(withPunctuationAndSplit4);

declare const withPunctuationAndNumber: KebabCase<'foo-bar::01'>;
expectType<'foo-bar::01'>(withPunctuationAndNumber);

declare const withPunctuationAndNumber2: KebabCase<'foo-bar::01', {splitOnNumbers: true}>;
expectType<'foo-bar::-01'>(withPunctuationAndNumber2);
declare const withPunctuationSplitAndNumber: KebabCase<'foo-bar::01', {splitOnPunctuation: true}>;
expectType<'foo-bar-01'>(withPunctuationSplitAndNumber);

declare const withPunctuationSplitAndNumberSplit: KebabCase<'foo-bar::01', {splitOnPunctuation: true; splitOnNumbers: true}>;
expectType<'foo-bar-01'>(withPunctuationSplitAndNumberSplit);

declare const withPunctuationAndNumberSplit2: KebabCase<'foo-bar::01', {splitOnNumbers: true}>;
expectType<'foo-bar::-01'>(withPunctuationAndNumberSplit2);

declare const withPunctuationSplitAndNumberSplit2: KebabCase<'foo-bar::01', {splitOnNumbers: true; splitOnPunctuation: true}>;
expectType<'foo-bar-01'>(withPunctuationSplitAndNumberSplit2);
17 changes: 17 additions & 0 deletions test-d/kebab-cased-properties-deep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,23 @@ type User = {
regExp: RegExp;
};

type UserPunctuated = {
'user::id': number;
'user::name': string;
date: Date;
'reg::exp': RegExp;
};

type UserWithFriends = {
userInfo: User;
userFriends: User[];
};

type UserWithFriendsPunctuated = {
'user@info': UserPunctuated;
'user#friends': UserPunctuated[];
};

const result: KebabCasedPropertiesDeep<UserWithFriends> = {
'user-info': {
'user-id': 1,
Expand All @@ -44,10 +56,15 @@ const result: KebabCasedPropertiesDeep<UserWithFriends> = {
},
],
};

expectType<KebabCasedPropertiesDeep<UserWithFriends>>(result);
expectType<KebabCasedPropertiesDeep<UserWithFriendsPunctuated, {splitOnPunctuation: true}>>(result);

expectType<{'foo-bar': unknown}>({} as KebabCasedPropertiesDeep<{foo_bar: unknown}>);
expectType<{'foo-bar': {'bar-baz': unknown}; biz: unknown}>({} as KebabCasedPropertiesDeep<{foo_bar: {bar_baz: unknown}; biz: unknown}>);

expectType<{'foo-bar': any}>({} as KebabCasedPropertiesDeep<{foo_bar: any}>);
expectType<{'foo-bar': {'bar-baz': any}; biz: any}>({} as KebabCasedPropertiesDeep<{foo_bar: {bar_baz: any}; biz: any}>);

expectType<{'foo-bar': unknown}>({} as KebabCasedPropertiesDeep<{'foo::bar': unknown}, {splitOnPunctuation: true}>);
expectType<{'foo-bar': {'bar-baz': unknown}; biz: unknown}>({} as KebabCasedPropertiesDeep<{'foo::bar': {'bar@baz': unknown}; biz: unknown}, {splitOnPunctuation: true}>);
Loading