Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

TypeScript convert generic object from snake to camel case

Tags:

typescript

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,
  ...
}
like image 438
mattnedrich Avatar asked Feb 17 '20 20:02

mattnedrich


People also ask

Which is better CamelCase or snake case?

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.

How do I convert a string to a CamelCase in TypeScript?

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.

Does TypeScript use CamelCase?

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.


1 Answers

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.

like image 159
ford04 Avatar answered Sep 20 '22 09:09

ford04