typescripttypescript-typings

Convert `PascalCase` to `strictCamelCase` in TypeScript type


Using the builtin Uncapitalize<S>, you can naively transform a string such as FooBar to fooBar:

const fooBar: Uncapitalize<'FooBar'> = 'fooBar';

However, this method falls short when you use class names following the HTML5 convention, such as XMLHttpRequest:

const xmlHttpRequest: Uncapitalize<'XMLHttpRequest'> = 'xMLHttpRequest';

Specifically, I'm looking for a type that can:

  1. Transform instances of PascalCase that contains mutliple conjoined UPPER_CASE letters that form an acronym, into a strictCamelCase string type that lowercases the acronyms,
  2. Without multiple capital letter humps.

For example:

Since I'll be using this to validate code, not transform user input, I don't really care if it's ugly like that last example, because that becomes the consuming developer's problem to fix their naming (:

I know I can use the new template literal types in TypeScript, specifically, using their inference features. However, their examples only show one-sided inference, like ${infer Foo}bar, & using multiple inference types make seem to make the last one greedy, like ${infer FirstChar}${infer Rest}bar.


Solution

  • You can use recursion on the type level to get around the greediness problem.

    type StrictCase<S extends string> =
      S extends `${
        infer P1 extends Uppercase<string>
      }${
        infer P2 extends Uppercase<string>
      }${
        infer P3
      }`
        ? `${Lowercase<P1>}${StrictCase<`${P2}${P3}`>}`
        : S extends `${infer P1}${infer P2}`
        ? P2 extends ''
          ? Lowercase<P1>
          : `${P1}${StrictCase<P2>}`
        : S;
    
    type StrictPascalCase<S extends string> = Capitalize<StrictCase<S>>
    type StrictCamelCase<S extends string> = Uncapitalize<StrictCase<S>>;
    

    In the StrictCase<S> type:

    1. If there is two conjoined uppercase letters -> FOOBar
      1. This is the set of uppercase letters that were problematic in the question
        Transform the first char into lowercase, then pass the rest back into StrictCase<S>. -> fStrictCamelCaseImpl<'OOBar'>
    2. Else if there is only one letter left -> r
      1. Lowercase & return it. -> r
    3. Else if there are at least two letters -> Bar
      1. These can be any combination of cases
        Return the first char as is, then pass the rest back into StrictCase<S>. -> BStrictCamelCaseImpl<'ar'>
    4. Else -> ar
      1. Return as is -> ar

    Then, after that is done, to normalise into either camelCase or PascalCase, it's just a matter of Capitalize<S> or Uncapitalize<S>, which transforms the first letter either into UPPER_CASE or lowercase.