Issue
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.
Solution
Update: TS4.5 will introduce tail recursion elimination on conditional types, meaning that it will soon be 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 Uppercase<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.
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 Uppercase<C0> ? "_" : ""}${Lowercase<C0>}${CamelToSnake<R>}` :
"";
Here we are parsing T
character-by-character. If the character is uppercase, 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 = {
abcdefghijklmno: boolean;
abcdefghijklmno_p: boolean;
} */
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 Uppercase<C0> ? "_" : ""}${Lowercase<C0>}${C1 extends Uppercase<C1> ? "_" : ""}${Lowercase<C1>}${CamelToSnake<R>}` :
T extends `${infer C0}${infer R}` ?
`${C0 extends Uppercase<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 = {
abcdefghijklmno: boolean;
abcdefghijklmnop: boolean;
abcdefghijklmnopq: boolean;
abcdefghijklmnopqrstuvwxyzabcd: boolean;
abcdefghijklmnopqrstuvwxyzabcd_e: boolean;
abcdefghijklmnopqrstuvwxyzabcd_e_f: boolean;
}*/
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.
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.