Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add TitleCase and Titleize #303

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ export {DelimiterCasedProperties} from './source/delimiter-cased-properties';
export {DelimiterCasedPropertiesDeep} from './source/delimiter-cased-properties-deep';
export {Join} from './source/join';
export {Split} from './source/split';
export {TitleCase} from './source/title-case';
export {Titleize} from './source/titleize';
export {Trim} from './source/trim';
export {Includes} from './source/includes';
export {Get} from './source/get';
Expand Down
2 changes: 2 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ Click the type names for complete docs.
- [`DelimiterCasedPropertiesDeep`](source/delimiter-cased-properties-deep.d.ts) – Convert object properties to a custom string delimiter casing recursively.
- [`Join`](source/join.d.ts) - Join an array of strings and/or numbers using the given string as a delimiter.
- [`Split`](source/split.d.ts) - Represents an array of strings split using a given character or character set.
- [`TitleCase`](source/title-case.d.ts) - Recursively convert string literal to APA title case (`This is an Example of Title Cased Format`).
- [`Titleize`](source/titleize.d.ts) - Recursively convert string literal to "Titleized" format (`This Is An Example Of Titleized Format`).
- [`Trim`](source/trim.d.ts) - Remove leading and trailing spaces from a string.
- [`Get`](source/get.d.ts) - Get a deeply-nested property from an object using a key path, like [Lodash's `.get()`](https://lodash.com/docs/latest#get) function.
- [`LastArrayElement`](source/last-array-element.d.ts) - Extracts the type of the last element of an array.
Expand Down
66 changes: 66 additions & 0 deletions source/title-case.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {StringDigit, UpperCaseCharacters} from './utilities';
import {SplitIncludingDelimiters} from './delimiter-case';
import {CamelCase} from './camel-case';
import {Join} from './join';

export type Conjunctions = 'and' | 'as' | 'but' | 'for' | 'if' | 'nor' | 'or' | 'so' | 'yet';
export type Articles = 'a' | 'an' | 'the';
export type ShortPrepositions = 'as' | 'at' | 'by' | 'for' | 'in' | 'of' | 'off' | 'on' | 'per' | 'to' | 'up' | 'via' | 'is' | 'ok';

export type ApaIgnoreUpperCase = Conjunctions | Articles | ShortPrepositions;

type ApaTitleCase<K, I extends ApaIgnoreUpperCase> = K extends string ? Lowercase<K> extends I ? Lowercase<K> : Capitalize<K> : K;

type TitleCaseJoin<Parts extends readonly any[], I extends ApaIgnoreUpperCase> = Parts extends [`${infer FirstPart}`, `${infer SecondPart}`, ...infer Rest]
? FirstPart extends StringDigit
? `${ApaTitleCase<FirstPart, I>}${TitleCase<Join<[SecondPart, Rest extends string[] | number[] ? Join<Rest, ''> : ''], ''>>}`
: `${ApaTitleCase<FirstPart, I>} ${TitleCase<Join<[SecondPart, Rest extends string[] | number[] ? Join<Rest, ''> : ''], ''>>}`
: Parts extends [`${infer FirstPart}`, ...infer Rest]
? `${ApaTitleCase<FirstPart, I>}${Rest extends string ? Rest : TitleCaseJoin<Rest, I>}`
: Parts extends [string | number] ? Parts extends [string] ? Capitalize<Parts[0]> : `${Parts[0]}`
: '';

/**
Convert a string literal to Title Case.

__This is similar to 'Titleize', however does follow APA guidelines for title casing.__

- _typeOfAnimal -> Type Of Animal (Titleize)_
- _typeOfAnimal -> Type of Animal (Title Case)_

This can be useful when, for example, converting a camel-cased object property into Title Case for data display within a table or page.
see https://apastyle.apa.org/style-grammar-guidelines/capitalization/title-case for more information about when to use title case according to the APA.

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

// Simple

const someVariable: TitleCase<'fooBar'> = 'Foo Bar';

// Advanced

interface CustomObject {
id: number;
fooBar: string;
thisIsASpecialProperty: any;
}

type CustomObjectProps = keyof CustomObject;

interface CustomGridColDef extends GridColDef {
field: CustomObjectProps;
headerName: TitleCase<CustomObjectProps>;
}

const columns: CustomGridColDef[] = [
{ field: 'id', headerName: 'Id' },
{ field: 'name', headerName: 'Name' },
{ field: 'thisIsASpecialProperty', headerName: 'This is a Special Property' }
]
```

@category Template Literals
*/
export type TitleCase<K, I extends ApaIgnoreUpperCase = ApaIgnoreUpperCase> = K extends string ? TitleCaseJoin<SplitIncludingDelimiters<CamelCase<K>, UpperCaseCharacters | StringDigit>, I> : K;
72 changes: 72 additions & 0 deletions source/titleize.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import {StringDigit, UpperCaseCharacters} from './utilities';
import {SplitIncludingDelimiters} from './delimiter-case';
import {CamelCase} from './camel-case';
import {Join} from './join';

type TitleizeJoin<Parts extends readonly any[]> = Parts extends [`${infer FirstPart}`, `${infer SecondPart}`, ...infer Rest]
? FirstPart extends StringDigit
? `${Capitalize<FirstPart>}${Titleize<Join<[SecondPart, Rest extends string[] | number[] ? Join<Rest, ''> : ''], ''>>}`
: `${Capitalize<FirstPart>} ${Titleize<Join<[SecondPart, Rest extends string[] | number[] ? Join<Rest, ''> : ''], ''>>}`
: Parts extends [`${infer FirstPart}`, ...infer Rest] ? `${Capitalize<FirstPart>}${Rest extends string ? Rest : TitleizeJoin<Rest>}`
: Parts extends [string | number] ? Parts extends [string] ? Capitalize<Parts[0]> : `${Parts[0]}` : '';

/**
Convert a string literal to Titleized Format.

__This is similar to 'TitleCase', however does _not_ follow APA guidelines for title casing:__

- _typeOfAnimal -> Type Of Animal (Titleize)_
- _typeOfAnimal -> Type of Animal (Title Case)_

This can be useful when converting a camel-cased object property into Titleized format for data display within a table or page.

It is ideal to use Titleize when you want automated formatting of data for display with a utility function like below:

```ts
const titleize = (str: string) => str.replace(/([A-Z])/g, ' $1').replace(/^./, (str) => str.toUpperCase());
```

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

// Simple

const someVariable: Titleize<'fooBar'> = 'Foo Bar';

// Advanced

interface CustomObject {
id: number;
fooBar: string;
prettyPrintMePlease: any;
}

type CustomObjectProps = keyof CustomObject;

interface CustomGridColDef extends GridColDef {
field: CustomObjectProps;
headerName: Titleize<CustomObjectProps>;
}

const columns: CustomGridColDef[] = [
{ field: 'id', headerName: 'Id' },
{ field: 'name', headerName: 'Name' },
{ field: 'prettyPrintMePlease', headerName: 'Pretty Print Me Please' }
]

// Or a more automated approach
const apiObject: CustomObject = {
id: 1,
fooBar: 'barBaz',
prettyPrintMePlease: null
}

const columns = Object(apiObject).keys().map(key => ({ field: key, headerName: titleize(key) }));
```

@category Template Literals
*/
export type Titleize<K> = K extends string
? TitleizeJoin<SplitIncludingDelimiters<CamelCase<K>, UpperCaseCharacters | StringDigit>>
: K;
15 changes: 15 additions & 0 deletions test-d/title-case.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {expectType} from 'tsd';
import {TitleCase} from '../index';
import {ApaIgnoreUpperCase} from '../source/title-case';

const apaTitleFromCamel: TitleCase<'fooBarIsGreat'> = 'Foo Bar is Great';
expectType<'Foo Bar is Great'>(apaTitleFromCamel);

const apaTitleFromKebabWithCustomCaseIgnore: TitleCase<'bar-baz-is-ok', ApaIgnoreUpperCase | 'ok'> = 'Bar Baz is ok';
expectType<'Bar Baz is ok'>(apaTitleFromKebabWithCustomCaseIgnore);

const apaTitleFromComplexKebab: TitleCase<'foo-bar-abc-123'> = 'Foo Bar Abc 123';
expectType<'Foo Bar Abc 123'>(apaTitleFromComplexKebab);

const apaTitleFromSnake: TitleCase<'foo_bar_abc'> = 'Foo Bar Abc';
expectType<'Foo Bar Abc'>(apaTitleFromSnake);
14 changes: 14 additions & 0 deletions test-d/titleize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import {expectType} from 'tsd';
import {Titleize} from '../index';

const titleFromCamel: Titleize<'fooBarIsGreat'> = 'Foo Bar Is Great';
expectType<'Foo Bar Is Great'>(titleFromCamel);

const titleFromKebab: Titleize<'bar-baz-is-ok'> = 'Bar Baz Is Ok';
expectType<'Bar Baz Is Ok'>(titleFromKebab);

const titleFromComplexKebab: Titleize<'foo-bar-abc-123'> = 'Foo Bar Abc 123';
expectType<'Foo Bar Abc 123'>(titleFromComplexKebab);

const titleFromSnake: Titleize<'foo_bar_abc'> = 'Foo Bar Abc';
expectType<'Foo Bar Abc'>(titleFromSnake);