I would like to write a function that accepts an object with snake case keys and converts it to an object with camel case keys. What is the best way such a function could be typed in TypeScript, assuming we know the type of the input object, but want the solution to be generic.
type InputType = {
snake_case_key_1: number,
snake_case_key_2: string,
...
}
function snakeToCamelCase(object: T): U {
...
}
What is the best job that could be done to type T
and U
.
I would like U
to be as narrowly typed as possible, and the type of U
to be based on T
ideally.
Ideally, if T
is my example InputType
I would like U to be typed as
{
snakeCaseKey1: number,
snakeCaseKey2: string,
...
}
Snake casing is generally considered to be more readable, as each word is separated by a space and it's easy to see where one word ends and the next begins. However, camel casing/pascal casing can make code easier to read for those who are familiar with the programming language in question.
Approach: Use str. replace() method to replace the first character of string into lower case and other characters after space will be into upper case. The toUpperCase() and toLowerCase() methods are used to convert the string character into upper case and lower case respectively.
Camel case usage in TypeScriptIn TypeScript & JavaScript, the camel case convention is used to signify that a token is a variable, function, method, parameter, or property.
Playground
This is possible with template literal types in TypeScript 4.1 (see also snake_case):
type SnakeToCamelCase<S extends string> =
S extends `${infer T}_${infer U}` ?
`${T}${Capitalize<SnakeToCamelCase<U>>}` :
S
type T11 = SnakeToCamelCase<"hello"> // "hello"
type T12 = SnakeToCamelCase<"hello_world"> // "helloWorld"
type T13 = SnakeToCamelCase<"hello_ts_world"> // "helloTsWorld"
type T14 = SnakeToCamelCase<"hello_world" | "foo_bar">// "helloWorld" | "fooBar"
type T15 = SnakeToCamelCase<string> // string
type T16 = SnakeToCamelCase<`the_answer_is_${N}`>//"theAnswerIs42" (type N = 42)
You then will be able to use key remapping in mapped types to construct a new record type:
type OutputType = {[K in keyof InputType as SnakeToCamelCase<K>]: InputType[K]}
/*
type OutputType = {
snakeCaseKey1: number;
snakeCaseKey2: string;
}
*/
type CamelToSnakeCase<S extends string> =
S extends `${infer T}${infer U}` ?
`${T extends Capitalize<T> ? "_" : ""}${Lowercase<T>}${CamelToSnakeCase<U>}` :
S
type T21 = CamelToSnakeCase<"hello"> // "hello"
type T22 = CamelToSnakeCase<"helloWorld"> // "hello_world"
type T23 = CamelToSnakeCase<"helloTsWorld"> // "hello_ts_world"
Once you got above types, it is quite simple to convert between them and other cases by using intrinsic string types Capitalize
and Uncapitalize
:
type CamelToPascalCase<S extends string> = Capitalize<S>
type PascalToCamelCase<S extends string> = Uncapitalize<S>
type PascalToSnakeCase<S extends string> = CamelToSnakeCase<Uncapitalize<S>>
type SnakeToPascalCase<S extends string> = Capitalize<SnakeToCamelCase<S>>
For kebab case, replace _
of snake case type by -
.
type SnakeToCamelCaseNested<T> = T extends object ? {
[K in keyof T as SnakeToCamelCase<K & string>]: SnakeToCamelCaseNested<T[K]>
} : T
This error can happen with quite long strings. You can process multiple sub-terms in one go to limit type recursion to an acceptable range for the compiler. E.g. SnakeToCamelCaseXXL
:
Playground
type SnakeToCamelCaseXXL<S extends string> =
S extends `${infer T}_${infer U}_${infer V}` ?
`${T}${Capitalize<U>}${Capitalize<SnakeToCamelCaseXXL<V>>}` :
S extends `${infer T}_${infer U}` ?
`${T}${Capitalize<SnakeToCamelCaseXXL<U>>}` :
S
Note: In the first condition, T
and U
each infer one sub-term, while V
infers the rest of the string.
Update: TS 4.5 will raise type instantiation depth limit from 50 to 100, so this compiler trick is not necessary with newer versions. For more complex cases, you now can also use tail recursive evaluation.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With