๐ŸŽ“ 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?