Skip to content
Merged
Show file tree
Hide file tree
Changes from 15 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
15 changes: 10 additions & 5 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 @@ -49,8 +48,14 @@ import type {CamelCase} from 'type-fest';

// Simple

const someVariable: CamelCase<'foo-bar'> = 'fooBar';
const preserveConsecutiveUppercase: CamelCase<'foo-BAR-baz', {preserveConsecutiveUppercase: true}> = 'fooBARBaz';
type CamelCase1 = CamelCase<'foo-bar'>;
//=> 'fooBar'

type CamelCase2 = CamelCase<'foo-BAR-baz', {preserveConsecutiveUppercase: true}>;
//=> 'fooBARBaz'

type CamelCase3 = CamelCase<'foo-bar:BAZ', {splitOnPunctuation: true}>;
//=> 'fooBarBaz'

// Advanced

Expand Down
10 changes: 7 additions & 3 deletions source/kebab-case.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@ This can be useful when, for example, converting a camel-cased object property t
import type {KebabCase} from 'type-fest';

// Simple
type KebabCase1 = KebabCase<'fooBar'>;
//=> 'foo-bar'

const someVariable: KebabCase<'fooBar'> = 'foo-bar';
const someVariableNoSplitOnNumbers: KebabCase<'p2pNetwork', {splitOnNumbers: false}> = 'p2p-network';
type KebabCase2 = KebabCase<'p2pNetwork', {splitOnNumbers: false}>;
//=> 'p2p-network'

// Advanced
type KebabCase3 = KebabCase<'div.card::after', {splitOnPunctuation: true}>;
//=> 'div-card-after'

// Advanced
type KebabCasedProperties<T> = {
[K in keyof T as KebabCase<K>]: T[K]
};
Expand Down
10 changes: 8 additions & 2 deletions source/pascal-case.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,14 @@ import type {PascalCase} from 'type-fest';

// Simple

const someVariable: PascalCase<'foo-bar'> = 'FooBar';
const preserveConsecutiveUppercase: PascalCase<'foo-BAR-baz', {preserveConsecutiveUppercase: true}> = 'FooBARBaz';
type PascalCase1 = PascalCase<'foo-bar'>;
//=> 'FooBar'

type PascalCase2 = PascalCase<'foo-BAR-baz', {preserveConsecutiveUppercase: true}>;
//=> 'FooBARBaz'

type PascalCase3 = PascalCase<'foo-bar>>baz', {splitOnPunctuation: true}>;
//=> 'FooBarBaz'

// Advanced

Expand Down
10 changes: 7 additions & 3 deletions source/snake-case.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,14 @@ This can be useful when, for example, converting a camel-cased object property t
import type {SnakeCase} from 'type-fest';

// Simple
type SnakeCase1 = SnakeCase<'fooBar'>;
//=> 'foo_bar'

const someVariable: SnakeCase<'fooBar'> = 'foo_bar';
const noSplitOnNumbers: SnakeCase<'p2pNetwork'> = 'p2p_network';
const splitOnNumbers: SnakeCase<'p2pNetwork', {splitOnNumbers: true}> = 'p_2_p_network';
type SnakeCase2 = SnakeCase<'p2pNetwork', {splitOnNumbers: false}>;
//=> 'p2p_network'

type SnakeCase3 = SnakeCase<'div.card::after', {splitOnPunctuation: true}>;
//=> 'div_card_after'

// Advanced

Expand Down
47 changes: 38 additions & 9 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,55 +39,83 @@ 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
```
import type {Words} from 'type-fest';

type Words0 = Words<'helloWorld'>;
type Words1 = Words<'helloWorld'>;
//=> ['hello', 'World']

type Words1 = Words<'helloWORLD'>;
type Words2 = Words<'helloWORLD'>;
//=> ['hello', 'WORLD']

type Words2 = Words<'hello-world'>;
type Words3 = Words<'hello-world'>;
//=> ['hello', 'world']

type Words3 = Words<'--hello the_world'>;
type Words4 = Words<'--hello the_world'>;
//=> ['hello', 'the', 'world']

type Words4 = Words<'lifeIs42'>;
type Words5 = Words<'lifeIs42'>;
//=> ['life', 'Is', '42']

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

type Words7 = Words<'hello:world'>;
//=> ['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
10 changes: 10 additions & 0 deletions test-d/camel-case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,13 @@ 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');
27 changes: 14 additions & 13 deletions test-d/kebab-case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,20 +79,21 @@ expectType<'fo-o2bar'>(kebabFromNumberInTheMiddleNoSplitOnNumbersEdgeCase2);
const kebabFromNumberInTheMiddleNoSplitOnNumbersEdgeCase3: KebabCase<'FOO22Bar'> = 'foo22-bar';
expectType<'foo22-bar'>(kebabFromNumberInTheMiddleNoSplitOnNumbersEdgeCase3);

declare const withPunctuation: KebabCase<'onDialog:close'>;
expectType<'on-dialog:close'>(withPunctuation);
// Punctuation
expectType<KebabCase<'onDialog:close'>>('on-dialog:close');
expectType<KebabCase<'foo-bar>>baz'>>('foo-bar>>baz');
expectType<KebabCase<'card::after'>>('card::after');
expectType<KebabCase<'div.card::after'>>('div.card::after');

declare const withPunctuation2: KebabCase<'foo-bar>>baz'>;
expectType<'foo-bar>>baz'>(withPunctuation2);
expectType<KebabCase<'onDialog:close', {splitOnPunctuation: true}>>('on-dialog-close');
expectType<KebabCase<'foo-bar>>baz', {splitOnPunctuation: true}>>('foo-bar-baz');
expectType<KebabCase<'card::after', {splitOnPunctuation: true}>>('card-after');
expectType<KebabCase<'div.card::after', {splitOnPunctuation: true}>>('div-card-after');

declare const withPunctuation3: KebabCase<'card::after'>;
expectType<'card::after'>(withPunctuation3);
// Punctuation with number
expectType<KebabCase<'foo-bar::01'>>('foo-bar::01');
expectType<KebabCase<'foo-bar::01', {splitOnNumbers: true}>>('foo-bar::-01');

declare const withPunctuation4: KebabCase<'div.card::after'>;
expectType<'div.card::after'>(withPunctuation4);
expectType<KebabCase<'foo-bar::01', {splitOnPunctuation: true; splitOnNumbers: false}>>('foo-bar-01');
expectType<KebabCase<'foo-bar::01', {splitOnNumbers: true; splitOnPunctuation: true}>>('foo-bar-01');

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);
11 changes: 11 additions & 0 deletions test-d/pascal-case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,14 @@ expectType<PascalCase<'foo\tBAR-Biz_BUZZ'>>('FooBarBizBuzz');

expectType<PascalCase<string, {preserveConsecutiveUppercase: true}>>({} as Capitalize<string>);
expectType<PascalCase<string>>({} as Capitalize<string>);

// Punctuation
expectType<PascalCase<'onDialog:close'>>('OnDialog:close');
expectType<PascalCase<'foo-bar>>baz'>>('FooBar>>baz');
expectType<PascalCase<'foo-bar::01'>>('FooBar::01');

expectType<PascalCase<'onDialog:close', {splitOnPunctuation: true}>>('OnDialogClose');
expectType<PascalCase<'foo-bar>>baz', {splitOnPunctuation: true}>>('FooBarBaz');
expectType<PascalCase<'fooBAR:biz', {splitOnPunctuation: true; preserveConsecutiveUppercase: true}>>('FooBARBiz');
expectType<PascalCase<'foo-bar::01', {splitOnPunctuation: true}>>('FooBar01');

35 changes: 19 additions & 16 deletions test-d/snake-case.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,20 +82,23 @@ expectType<'fo_o2bar'>(snakeFromNumberInTheMiddleNoSplitOnNumbersEdgeCase2);
const snakeFromNumberInTheMiddleNoSplitOnNumbersEdgeCase3: SnakeCase<'FOO22Bar'> = 'foo22_bar';
expectType<'foo22_bar'>(snakeFromNumberInTheMiddleNoSplitOnNumbersEdgeCase3);

declare const withPunctuation: SnakeCase<'onDialog:close'>;
expectType<'on_dialog:close'>(withPunctuation);
// Punctuation
expectType<SnakeCase<'onDialog:close'>>('on_dialog:close');
expectType<SnakeCase<'foo-bar>>baz'>>('foo_bar>>baz');
expectType<SnakeCase<'card::after'>>('card::after');
expectType<SnakeCase<'div.card::after'>>('div.card::after');
expectType<SnakeCase<'foo-bar::01'>>('foo_bar::01');

expectType<SnakeCase<'onDialog:close', {splitOnPunctuation: true}>>('on_dialog_close');
expectType<SnakeCase<'foo-bar>>baz', {splitOnPunctuation: true}>>('foo_bar_baz');
expectType<SnakeCase<'card::after', {splitOnPunctuation: true}>>('card_after');
expectType<SnakeCase<'div.card::after', {splitOnPunctuation: true}>>('div_card_after');
expectType<SnakeCase<'foo-bar::01', {splitOnPunctuation: true}>>('foo_bar_01');

// Punctuation with number
expectType<SnakeCase<'foo-bar::01'>>('foo_bar::01');
expectType<SnakeCase<'foo-bar::01', {splitOnNumbers: true}>>('foo_bar::_01');

expectType<SnakeCase<'foo-bar::01', {splitOnPunctuation: true; splitOnNumbers: false}>>('foo_bar_01');
expectType<SnakeCase<'foo-bar::01', {splitOnNumbers: true; splitOnPunctuation: true}>>('foo_bar_01');

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

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

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

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

declare const withPunctuationAndNumber2: SnakeCase<'foo-bar::01', {splitOnNumbers: true}>;
expectType<'foo_bar::_01'>(withPunctuationAndNumber2);
15 changes: 15 additions & 0 deletions test-d/words.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,18 @@ expectType<Words<'01item01'>>(['01', 'item', '01']);
expectType<Words<'10item10'>>(['10', 'item', '10']);
expectType<Words<'010item010'>>(['010', 'item', '010']);
expectType<Words<'item0_item_1 item -2'>>(['item', '0', 'item', '1', 'item', '2']);

// SplitOnPunctuation
expectType<Words<':'>>([':']);
expectType<Words<'::'>>([':', ':']);
expectType<Words<'hello::'>>(['hello', ':', ':']);
expectType<Words<'hello:world'>>(['hello', ':world']);
expectType<Words<'hello::world'>>(['hello', ':', ':world']);
expectType<Words<'hello-braveNew:world'>>(['hello', 'brave', 'New', ':world']);

expectType<Words<':', {splitOnPunctuation: true}>>([]);
expectType<Words<'::', {splitOnPunctuation: true}>>([]);
expectType<Words<'hello::', {splitOnPunctuation: true}>>(['hello']);
expectType<Words<'hello:world', {splitOnPunctuation: true}>>(['hello', 'world']);
expectType<Words<'hello::world', {splitOnPunctuation: true}>>(['hello', 'world']);
expectType<Words<'hello-braveNew:world', {splitOnPunctuation: true}>>(['hello', 'brave', 'New', 'world']);