Question

TypeScript convert generic object from snake to camel case

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,
  ...
}
 48  29605  48
1 Jan 1970

Solution

 117

Solution

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;
  }
*/

Extensions

Inversion type

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"

Pascal case, Kebab case and inversions

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

Convert nested properties

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

"Type instantiation is excessively deep and possibly infinite."

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.

2021-01-09