Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is it possible to use mapped types in Typescript to change a types key names?

Tags:

typescript

I have a ton of generated typescript types from proto files. These types properties are in camel case and my proto files (and api) are in snake case.

I would like to avoid transforming my api data to camel case in order to satisfy my type constraints. I am trying to figure out a way to use mapped types to change a types keys from camel to snake case.

For example:

Generated Type

type g = {
    allTheNames: string
}

type SnakePerson = {
    firstName: string
    lastName: string
    name: g

Desired Type

{
  first_name: string
  last_name: string
  g: { all_the_names: string }
}

I made an attempt but I am fairly new to typescript and mapped types

type ToSnakeCase<K extends string, T> = {
  [snakeCase([P in K])]: T[P]
}

Any help including telling me this is not possible would be much appreciated.

like image 331
tnyN Avatar asked Feb 01 '26 20:02

tnyN


1 Answers

Update for TS4.5+

Now TypeScript has tail recursion elimination on conditional types, meaning that it is possible to write a version of CamelToSnake which can operate on long strings without running into recursion depth limits, as the compiler will be able to evaluate the type iteratively instead of recursively. Here's a version that will work:

type CamelToSnake<T extends string, P extends string = ""> = string extends T ? string :
    T extends `${infer C0}${infer R}` ?
    CamelToSnake<R, `${P}${C0 extends Lowercase<C0> ? "" : "_"}${Lowercase<C0>}`> : P

And you can test it out on... well, quite long strings, and it works flawlessly!

type Wow = CamelToSnake<"itWasTheBestOfTimesItWasTheWorstOfTimesItWasTheAgeOfWisdomItWasTheAgeOfFoolishnessItWasTheEpochOfBeliefItWasTheEpochOfIncredulityItWasTheSeasonOfLightItWasTheSeasonOfDarknessItWasTheSpringOfHopeItWasTheWinterOfDespairWeHadEverythingBeforeUsWeHadNothingBeforeUsWeWereAllGoingDirectToHeavenWeWereAllGoingDirectTheOtherWayInShortThePeriodWasSoFarLikeThePresentPeriodThatSomeOfItsNoisiestAuthoritiesInsistedOnItsBeingReceivedForGoodOrForEvilInTheSuperlativeDegreeOfComparisonOnly">
// type Wow = "it_was_the_best_of_times_it_was_the_worst_of_times_it_was_the_age_of_wisdom_it_was_the_age_of_foolishness_it_was_the_epoch_of_belief_it_was_the_epoch_of_incredulity_it_was_the_season_of_light_it_was_the_season_of_darkness_it_was_the_spring_of_hope_it_was_the_winter_of_despair_we_had_everything_before_us_we_had_nothing_before_us_we_were_all_going_direct_to_heaven_we_were_all_going_direct_the_other_way_in_short_the_period_was_so_far_like_the_present_period_that_some_of_its_noisiest_authorities_insisted_on_its_being_received_for_good_or_for_evil_in_the_superlative_degree_of_comparison_only"

You will be able to use type CamelKeysToSnake<T> or RecursiveSnakification<T> below as before.

Playground link to code


Original answer for TypeScript 4.1 through 4.4:

TypeScript 4.1's introduction of template literal types and mapped as clauses and recursive conditional types does allow you to implement a type function to convert camel-cased object keys to snake-cased keys, although this sort of string-parsing code tends to be difficult on the compiler and hits some rather shallow limits, unfortunately.

First we need a CamelToSnake<T> that takes a camel-cased string literal for T and produces a snake-cased version. The "simplest" implementation of that looks something like:

type CamelToSnake<T extends string> = string extends T ? string :
    T extends `${infer C0}${infer R}` ? 
    `${C0 extends Lowercase<C0> ? "" : "_"}${Lowercase<C0>}${CamelToSnake<R>}` :
    "";

Here we are parsing T character-by-character. If the character is not lowercase, we insert an underscore. Then we append a lowercase version of the character, and continue. Once we have SnakeToCase we can do the key mapping (using the as clauses in mapped types):

type CamelKeysToSnake<T> = {
    [K in keyof T as CamelToSnake<Extract<K, string>>]: T[K]
}

(Edit: if you need to map the keys recursively down through json-like objects, you can instead use

type RecursiveSnakification<T> = T extends readonly any[] ?
    { [K in keyof T]: RecursiveSnakification<T[K]> } :
    T extends object ? { 
      [K in keyof T as CamelToSnake<Extract<K, string>>]: RecursiveSnakification<T[K]> 
    } : T

but for the example type given in the question, a non-recursive mapped type will suffice. )

You can see this work on your example types:

interface SnakePerson {
    firstName: string
    lastName: string
}
   
type CamelPerson = CamelKeysToSnake<SnakePerson>
/* type CamelPerson = {
    first_name: string;
    last_name: string;
} */

Unfortunately, if your key names are longer than about fifteen characters, the compiler loses its ability to recurse with the simplest CamelToSnake implementation:

interface SnakeLengths {
    abcdefghijklmnO: boolean;
    abcdefghijklmnOP: boolean;
    abcdefghijklmnOPQ: boolean;
}

type CamelLengths = CamelKeysToSnake<SnakeLengths>
/* type CamelLengths = {
    abcdefghijklmn_o: boolean;
    abcdefghijklmn_op: boolean; // wrong!
    // gone!!!
} */

The sixteen-character key gets mapped incorrectly, and anything longer disappears entirely. To address this you can start making CamelToSnake more complicated; for example, to grab bigger chunks:

type CamelToSnake<T extends string> = string extends T ? string :
    T extends `${infer C0}${infer C1}${infer R}` ? 
    `${C0 extends Lowercase<C0> ? "" : "_"}${Lowercase<C0>}${C1 extends Lowercase<C1> ? "" : "_"}${Lowercase<C1>}${CamelToSnake<R>}` :
    T extends `${infer C0}${infer R}` ? 
    `${C0 extends Lowercase<C0> ? "" : "_"}${Lowercase<C0>}${CamelToSnake<R>}` :
    "";

This pulls off characters two-by-two instead of one-by-one, and only falls back to the one-by-one version if you have fewer than two characters left. This works for strings up to about 30 characters:

interface SnakeLengths {
    abcdefghijklmnO: boolean;
    abcdefghijklmnOP: boolean;
    abcdefghijklmnOPQ: boolean;
    abcdefghijklmnopqrstuvwxyzabcD: boolean
    abcdefghijklmnopqrstuvwxyzabcDE: boolean
    abcdefghijklmnopqrstuvwxyzabcDEF: boolean
    abcdefghijklmnopqrstuvwxyzabcDEFG: boolean
}


type CamelLengths = CamelKeysToSnake<SnakeLengths>
/* type CamelLengths = {
    abcdefghijklmn_o: boolean;
    abcdefghijklmn_o_p: boolean;
    abcdefghijklmn_o_p_q: boolean;
    abcdefghijklmnopqrstuvwxyzabc_d: boolean;
    abcdefghijklmnopqrstuvwxyzabc_de: boolean; // wrong!
    abcdefghijklmnopqrstuvwxyzabc_def: boolean; // wrong!
    // gone!
}*/

That's probably enough for most uses. If not, you could go back and try pulling off characters three at a time instead of two at a time. Or you could try to sidestep the character-by-character recursion and write something that breaks a string at the first uppercase character, like in this GitHub comment, but that runs into other similar issues.

The point is, TS4.1 gives you enough tools to pretty much do this, but not enough to do it without some tweaking and thought.

Playground link to code

like image 132
jcalz Avatar answered Feb 04 '26 13:02

jcalz



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!