🎓 A1Learner
🔍
0 of 37 lessons completed

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?