DEV Community

loading...
Cover image for How to UPPER_CASE to camelCase in raw Typescript generics

How to UPPER_CASE to camelCase in raw Typescript generics

svehla profile image Jakub Švehla Updated on ・5 min read

TLDR:

Today's challenge is to retype an UPPER_CASE static string into camelCase and apply this transformation recursively to the object keys.

Preview
Alt Text

Alt Text

As you can see, we transformed static type text written in UPPER_CASE format into camelCase. Then we applied the transformation recursively to all the object keys.

You can play with full source code here

Typescript 4.2 is already in beta version, so we should be prepared for new incoming features to fully use the power it offers. You can find all the new Typescript 4.2 features there: https://devblogs.microsoft.com/typescript/announcing-typescript-4-2-beta/

Let's deep dive into code

To change the case from UPPER_CASE to camel Case, we have to use the parser to convert uppercase letters to lowercase ones and to remove unwanted _.

Letters mapper

First of all, we create the Lower/Upper Mapper type which describes dependencies between lowercase and uppercase letters.

type LowerToUpperToLowerCaseMapper = {
  a: 'A'
  b: 'B'
  c: 'C'
  d: 'D'
  e: 'E'
  f: 'F'
  g: 'G'
  h: 'H'
  i: 'I'
  j: 'J'
  k: 'K'
  l: 'L'
  m: 'M'
  // ... and so on 
}

type UpperToLowerCaseMapper = {
  A: 'a'
  B: 'b'
  C: 'c'
  // ... and so on 
}

Enter fullscreen mode Exit fullscreen mode

Parse strings Utils

We have to write a small parser that will read UPPER_CASE format and try to parse it to the new structure which will be transformed into camelCase. So let's start with a text parser util function.

HeadLetter

This generic infers the first letter and just returns it.

type HeadLetter<T> = T extends `${infer FirstLetter}${infer _Rest}` ? FirstLetter : never

Enter fullscreen mode Exit fullscreen mode

Alt Text

TailLetters

This generic infers all the letters except the first one and returns them.

type TailLetters<T> = T extends `${infer _FirstLetter}${infer Rest}` ? Rest : never
Enter fullscreen mode Exit fullscreen mode

Alt Text

LetterToUpper

This generic calls the proper LowerCase Mapper structure to convert one char.

type LetterToUpper<T> = T extends `${infer FirstLetter}${infer _Rest}`
  ? FirstLetter extends keyof LowerToUpperToLowerCaseMapper
    ? LowerToUpperToLowerCaseMapper[FirstLetter]
    : FirstLetter
  : T
Enter fullscreen mode Exit fullscreen mode

Alt Text

LetterToLower

type LetterToLower<T> = T extends `${infer FirstLetter}${infer _Rest}`
  ? FirstLetter extends keyof UpperToLowerCaseMapper
    ? UpperToLowerCaseMapper[FirstLetter]
    : FirstLetter
  : T
Enter fullscreen mode Exit fullscreen mode

Alt Text

ToLowerCase

Now we're albe to recursively call HeadLetter, Tail and LetterToLower to iterate through the whole string and apply lowecase to them.


type ToLowerCase<T> = T extends ''
  ? T
  : `${LetterToLower<HeadLetter<T>>}${ToLowerCase<TailLetters<T>>}`

Enter fullscreen mode Exit fullscreen mode

Alt Text

ToSentenceCase

This generic transforms the first letter to uppercase and the rest of the letters to lowercase.

type ToSentenceCase<T> = `${LetterToUpper<HeadLetter<T>>}${ToLowerCase<TailLetters<T>>}`

Enter fullscreen mode Exit fullscreen mode

Alt Text

We're done with all our utils Generics, so we can jump into the final type implementation.

UpperCaseToPascalCase

We're almost there. Now we can write the generic which will transform CAMEL_CASE into PascalCase.

type ToPascalCase<T> = T extends ``
  ? T
  : T extends `${infer FirstWord}_${infer RestLetters}`
  ? `${ToSentenceCase<FirstWord>}${ToPascalCase<RestLetters>}`
  : ToSentenceCase<T>
Enter fullscreen mode Exit fullscreen mode

As you can see, we recursively split words by _ delimiter. Each word converts to Sentencecase and joins them together.

Alt Text

UpperCaseToCamelCase

The last step is to use PascalCase but to keep the first letter of the first word lowercase.

We use previously created generics and just combine them together.

export type UpperCaseToCamelCase<T> = `${ToLowerCase<HeadLetter<T>>}${TailLetters<ToPascalCase<T>>}`
Enter fullscreen mode Exit fullscreen mode

Alt Text

Pretty amazing and kinda simple code, right?

Apply case transformation to object keys

Now we want to build a static type that applies recursively UpperCaseToCamelCase generic to Object nested keys.

Before we start, let's define three helper generics.

GetObjValues

type GetObjValues<T> = T extends Record<any, infer V> ? V : never
Enter fullscreen mode Exit fullscreen mode

This simple generic helps us to extract data outside of a Record<any, T> wrapper.

Alt Text

Cast

This generic helps us to bypass the Typescript compiler to pass invalid types. We will use Cast to "shrink" a union type to the other type which is defined as the second parameter.

type Cast<T, U> = T extends U ? T : any
Enter fullscreen mode Exit fullscreen mode
type T4 = string | number
type T5 = Cast<T4, string>
Enter fullscreen mode Exit fullscreen mode

Alt Text

SwitchKeyValue

We use our previously defined generic GetObjValues<T> to switch the to the value.

The goal of this generic is to transform the string value into the key and vice-versa, like in the preview.

type Foo = SwitchKeyValue<{ a: 'key-a', b: 'key-b' }>
Enter fullscreen mode Exit fullscreen mode

Alt Text

type GetObjValues<T> = T extends Record<any, infer V> ? V : never

export type SwitchKeyValue<
  T,
  // step 1
  T1 extends Record<string, any> = {
    [K in keyof T]: { key: K; value: T[K] }
  },
  // step 2
  T2 = {
    [K in GetObjValues<T1>['value']]: Extract<GetObjValues<T1>, { value: K }>['key']
  }
> = T2
Enter fullscreen mode Exit fullscreen mode

The whole procedure takes, two steps so I decided to keep the code less nested and to save partial values into variables. Sub results variables are saved due to generic parameters. Thanks to that Typescript feature I can "save" the results of transformations into "variables" T1 and T2. This is a pretty useful pattern of writting static types with less nesting.

Everything works fine so let's dive into recursive nested keys transformation.

TransformKeysToCamelCase

Now we will combine the generics from the whole article into one single piece of art.


type TransformKeysToCamelCase<
  T extends Record<string, any>,
  T0 = { [K in keyof T]: UpperCaseToCamelCase<K> },
  T1 = SwitchKeyValue<T0>,
  T2 = {
    [K in keyof T1]:T[Cast<T1[K], string>]
  }
> = T2
Enter fullscreen mode Exit fullscreen mode
type NestedKeyRevert = TransformKeysToCamelCase<{
  FOO_BAR: string
  ANOTHER_FOO_BAR: true | number,
}>
Enter fullscreen mode Exit fullscreen mode

Alt Text

As you can see, the generic has 3 steps which are saved into T0, T1 and T2 variables.

The First step

The first step creates an Object type where keys are UPPER_CASE and values are just keys transformed into camelCase

T0 = { [K in keyof T]: UpperCaseToCamelCase<K> },
Enter fullscreen mode Exit fullscreen mode

Alt Text

The second step

The second step just applies the previously created generic and switch keys to values

T1 = SwitchKeyValue<T0>,
Enter fullscreen mode Exit fullscreen mode

Alt Text

The third step

The third step connects T1 with the data type from T.

T2 = { [K in keyof T1]: T[Cast<T1[K], string>] }
Enter fullscreen mode Exit fullscreen mode

Alt Text

Add nested deep recursion

To provide this, we will create a generic which will check if the value is of type Object and will call recursion.

type CallRecursiveTransformIfObj<T> = T extends Record<any, any> ? TransformKeysToCamelCase<T> : T

Enter fullscreen mode Exit fullscreen mode

And updates the third step of TransformKeysToCamelCase generic.


type TransformKeysToCamelCase<
  T extends Record<string, any>,
  T0 = { [K in keyof T]: UpperCaseToCamelCase<K> },
  T1 = SwitchKeyValue<T0>,
  T2 = { [K in keyof T1]: CallRecursiveTransformIfObj<T[Cast<T1[K], string>]> }
> = T2
Enter fullscreen mode Exit fullscreen mode

And voilà! 🎉🎉🎉

If we test the nested data structure as a generic parameter

type NestedKeyRevert = TransformKeysToCamelCase<{
  FOO_BAR: string
  ANOTHER_FOO_BAR: true | number,
  NESTED_KEY: {
    NEST_FOO: string
    NEST_BAR: boolean
  },
}>
Enter fullscreen mode Exit fullscreen mode

Alt Text

Everything works well.

Congratulations that you have read this article until the end. We successfully added a nested key case transformation which is a pretty advanced task in raw typescript.

You can play with the full source code here

Don't forget to 🫀 if you like this article.

Discussion (2)

pic
Editor guide
Collapse
faiwer profile image
Stepan Zubashev

That's pretty interesting. Thank you for the article.

But I think it'd be better to add a note that this solution isn't recommended in real world projects, because it might slow down the compilation process dramatically. Because this Type-Engine needs to interpretate all these types symbol-by-symbol, and if you have a plenty of them, it's better to write those types manually or by means code-generating.

We have in our applications several hundreds of types that we'd like to automatically convert from snake_case to camelCase. But we still wait until TS will support it properly (like it does with UPPERCASE and lowercase). Because the compilation process is too slow even without it :-D

But for small projects or small amount of types it solves the task.

Collapse
svehla profile image
Jakub Švehla Author

Yeah, 😄 you're right that this solution is not too fast and TS checked could be pretty stuck on large codebases projects.

I think that there is the other side of the coin if you look at the static types as an alternative for "unit tests" (especially for smaller codebase where 100% accuracy code is not needed). So it's pretty good to have only one source of truth in your codebase instead of taking care of your .js files and your tests (.spec.js|.test.js) files.

Another nice feature of this solution is that you can remove it and replace upper code with a simple any and your runtime javascript will still be Okey :D

But I have to agree that efficiency is not the best one... :(