DEV Community

Cover image for How to UPPER_CASE to camelCase in raw Typescript generics
Jakub Švehla
Jakub Švehla

Posted on • Edited on

How to UPPER_CASE to camelCase in raw Typescript generics

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>
```

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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/1rj0ypv0h3n6fa4c40aj.png)

### 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.

```typescript
export type UpperCaseToCamelCase<T> = `${ToLowerCase<HeadLetter<T>>}${TailLetters<ToPascalCase<T>>}`
```

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/z8896snhd26k8vttz4dr.png)


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
```typescript
type GetObjValues<T> = T extends Record<any, infer V> ? V : never
```

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

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/zmtkvxeken62a9fmqwl6.png)


#### Cast

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


```typescript
type Cast<T, U> = T extends U ? T : any
```

```typescript
type T4 = string | number
type T5 = Cast<T4, string>
```

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/cxe1if6azoxac9xllypt.png)

#### 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.

```typescript
type Foo = SwitchKeyValue<{ a: 'key-a', b: 'key-b' }>
```

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/jo3p5452hft697turzh1.png)


```typescript
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
```


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.


```typescript

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
```

```typescript
type NestedKeyRevert = TransformKeysToCamelCase<{
  FOO_BAR: string
  ANOTHER_FOO_BAR: true | number,
}>
```

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/cw1tvlcelhkxmf765osl.png)


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   

```typescript
T0 = { [K in keyof T]: UpperCaseToCamelCase<K> },
```

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/2k8simblt3582u22moly.png)

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

```typescript
T1 = SwitchKeyValue<T0>,
```

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/rxn79p8b4e0z3lsvvsre.png)


#### The third step

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

```typescript
T2 = { [K in keyof T1]: T[Cast<T1[K], string>] }
```

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/cw1tvlcelhkxmf765osl.png)

### 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.

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

```

And updates the third step of TransformKeysToCamelCase generic.

```typescript

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
```



And voilà! 🎉🎉🎉


If we test the nested data structure as a generic parameter

```typescript
type NestedKeyRevert = TransformKeysToCamelCase<{
  FOO_BAR: string
  ANOTHER_FOO_BAR: true | number,
  NESTED_KEY: {
    NEST_FOO: string
    NEST_BAR: boolean
  },
}>
```

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/de4h6qg9obi1fzm7zwd1.png)


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](https://www.typescriptlang.org/play?target=1#code/C4TwDgpgBAMg9gdwgJwCpwKpkmu8nIDCAhgM4QCyx2KUAvFAN4CwAUFFMQFxQDkAgrzYcARj14AhIeygBjcYWkcAJuIAiSqBHEBRTQDNxAMU0BzcQHFNAC3EAJTQEtxASU0ArcQClNAa3EA0poANuIwmgC24hSaAHbiAHKacOIA8ppg4gAKmgCO4gCKmsjiAEqapOIAyprA4qiaAK7iGJoAbuIAapoI4gDqmgAe4gAamiDiAJqaAF7iAFrSAL5sbKCQUFg46PgoJORUNMj0TMJQ-OLEmhLiIpqE4rKaauLKmjriEJpG4vqaFuJTJo7OJrJoXOJHJovOJ3JoAuJfJoYOJgpoKOIIpoEuJYppUuI4JosuIwJoCuJcppSuJkJoquJSJpUOJgJoMOJGppOuI2po+uIEJoRuJBppJuIQJp5uIZstVqw1uBoHYIMRlDAIMBgCgADyoAB8J1QWkGOtiylIUAABgASRiOWL6WhGRzIUjATXalBLe2O53HAD6pQgHqW1qgAH4oK73Z6tTrjjxYhA2iglRtUMRHMEvYnSPqjQwTRAzRALVa7Q6nbRA7GPXmfX6a8cQ2GI9G28AoMnU+nFax1tBG7gtnrDcbTebLTbmwGY26Gwmm9X58HQ8Bw2do-X497jqXp1bfBAQHB9LBECh0GPcLsiGRKNQcGcONH7zejjsrw+Ds+UAA2ruI4ALqvj2C5xiOZw8KgCpDrAy53j+haToe5YzlW-ououe6Jr6q61l2W4yDuuEjlOGHHqe56bF+eA-vsT5HOB0a3t+BBMYcOBAeRSFgTIHA8MBSEwVAcEDghHF7I+qHFpRFZ8Jo0YSUJs6MCO0nILqqrqiOhYGgRWlMfq2a5khBaGoZ1oKgA9LZkEelAwRIVAjhWo0RxQMgG5uVawQ-hm0DoFU5bmrIEAmRODBVppmBHDpaoakhBlGQxnGyVmOYjpZBrWfByriXAWRkLIxDBFFRbiQpmE2aR4liSWZaKVhLaOcAfRwMgyhLIGc60F2OUkW+6khWF5YRSZu6dd1hn2ugJWkGVFWyYNFn5TIsFwKFsThZFmUGgqpZgF13YIbeTHoCQEQQCt5ByaN6UyfdunJfuqXzWZOX6sVpXlZVG1sPZnDYMEIBQKQsTECeciPm5u1wFAcAiO4ECyMApBBVA+zAPqAA0mxVU1R6bFG1U8BgWMWFqqQo505WNKGD3E1RUAhrIXXKLqxCxCABPYccnRGtGnQQSmabIAVGwkMEwTs407qOGmqDIDzpD6F1EQuPotPuMzNVWuznPc7zBM8yAwviarsTq5rASnqQV3EDdd0QKhsFHYMJ3IGdhVVAgjjALI1j2yA9PBIzupnKgePRwAjAbbNo8bHrII6phm7zVUsIJUAAQE8NQCeZ4XqgIE8IwRenjwAQANxQG0DPaOJ+cgVAKwyEsscyKgABMJw5xwHD54X1PALr4eM5ZccGgBvCNxHXwgeXUA6Gaqvo7qY8T0308GgTlcL4zNft7PvDF7wAkcB3RO91LwXW7byARKHjtwNdt0mdHidG91uqp+nTOFtu4cFQAABgHnnAujoq4l3EivC6j4nYuxMgEI0Xd44nH9oHYOodJ5u3AfvaO-cGCVxHjA4utFUBxxXjLOWaMFakCVhAFWasNbP21rrfUAEcb6jjq3AmADYimANCBdBbBb4KixgkDcEBlChxDBLbsxZH7sJfg7ZBn9ZKDxjKkVIgYJD8BpBDYAadhFnH4AkVIqA7A6FKHWPRBijE8FMYzKAAAfKAsRGgRBECgEBUAEg6CqKgHQahAwBB0BKU4ucgkhIcQSExZjTDgTiagJxxiRBwDgC5HmZwMGsCWIdVgQA)

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

Enter fullscreen mode Exit fullscreen mode

Top comments (6)

Collapse
 
silence_64 profile image
leere • Edited

Just two generics can do this

type SnakeToCamelCase<S extends string> =
  S extends `${infer T}_${infer U}` ?
  `${Lowercase<T>}${Capitalize<SnakeToCamelCase<U>>}` :
  S

type SnakeToCamelCaseNested<T> = T extends object ? {
  [K in keyof T as SnakeToCamelCase<K & string>]: SnakeToCamelCaseNested<T[K]>
} : T

Enter fullscreen mode Exit fullscreen mode
Collapse
 
svehla profile image
Jakub Švehla • Edited

In the time of writing this article lowercase and capitalise genericks did not exist.

I keep the article without changes because I wanted to demonstrate how to transform strings with low level features like rekursive iterations etc...


Iteration over keys with as keyword looks pretty handy! I have to chekt it! 😈

It's very nice generic! Thanks for sharing it !!!

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

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... :(

Collapse
 
pranavdakshina profile image
Pranav Dakshinamurthy

Is there a way handle this if the type is an array? For example:

type Sample = {
  a_1: string;
  b_1: string;
  c_1: Array<{
    a_1: string;
    b_1: string;
  }>;
};
Enter fullscreen mode Exit fullscreen mode
Collapse
 
svehla profile image
Jakub Švehla

Yep of course

you an do recursion with if statement like:

T extends any[]  ? /* ........
Enter fullscreen mode Exit fullscreen mode

I use similar technique here for recursive deep nesting
dev.to/svehla/typescript-how-to-de...

I hope it can help you somehow 🙏