TypeScript String Manipulation Types
String manipulation types compute a new string literal type from an old one, character by character. A recursive conditional type walks the literal with infer and rebuilds it in a different case.
Snake case built by hand
SnakeCase reads the first character and the rest of the literal. When the rest starts with a capital, an underscore joins the two parts.
type SnakeCase<S extends string> =
S extends `${infer A}${infer B}`
? B extends Uncapitalize<B>
? `${Lowercase<A>}${SnakeCase<B>}`
: `${Lowercase<A>}_${SnakeCase<B>}`
: S;
const columnName: SnakeCase<'userProfileName'> = 'user_profile_name';
console.log(columnName);Pascal case built by hand
PascalCase splits the literal at each underscore. Each segment is capitalised and rejoined without the separator.
type PascalCase<S extends string> =
S extends `${infer Head}_${infer Rest}`
? `${Capitalize<Head>}${PascalCase<Rest>}`
: Capitalize<S>;
const componentName: PascalCase<'user_profile_card'> = 'UserProfileCard';
console.log(componentName);The built-in case utilities beside the hand builds
Uppercase and Lowercase from the intermediate course convert a whole literal in one step. The hand builds above walk the literal because they also change its separators.
type Shout = Uppercase<'save changes'>;
type Whisper = Lowercase<'SAVE CHANGES'>;
const loudLabel: Shout = 'SAVE CHANGES';
const softLabel: Whisper = 'save changes';
console.log(loudLabel);
console.log(softLabel);A value outside the built type is refused
The built type holds exactly one literal, the snake case form. Assigning the original camel case string fails the check.
type SnakeCase<S extends string> =
S extends `${infer A}${infer B}`
? B extends Uncapitalize<B>
? `${Lowercase<A>}${SnakeCase<B>}`
: `${Lowercase<A>}_${SnakeCase<B>}`
: S;
const badColumn: SnakeCase<'userProfile'> = 'userProfile';Example
type SnakeCase<S extends string> =
S extends `${infer A}${infer B}`
? B extends Uncapitalize<B>
? `${Lowercase<A>}${SnakeCase<B>}`
: `${Lowercase<A>}_${SnakeCase<B>}`
: S;
type PascalCase<S extends string> =
S extends `${infer Head}_${infer Rest}`
? `${Capitalize<Head>}${PascalCase<Rest>}`
: Capitalize<S>;
const columnName: SnakeCase<'createdAtDate'> = 'created_at_date';
const componentName: PascalCase<'date_picker_field'> = 'DatePickerField';
console.log(columnName);
console.log(componentName);Key points
- The walk splits a literal into its first character and the rest with two infer placeholders.
- The rest decides the boundary, since a capital first letter in the rest marks a new word.
- Recursion ends when the pattern no longer matches and the remaining literal is returned.
- Splitting on an underscore is the mirror walk, and each segment is capitalised as it is rejoined.
- The built-in case utilities from the intermediate course change the case of a whole string at once.
- A hand-built walk does more than change case, since it can also insert or remove separators.
In this example, the same field travels in both directions, since SnakeCase turns a camel case name into a database column and PascalCase turns a snake case name into a component name. Each assigned string is the single literal its type computes.
Try it Yourself
Edit the code, then press Run.
Running is simulated · TypeScript Playground
Exercise
What single literal does SnakeCase<'orderTotalPrice'> hold?