Search code examples
typescript

TypeScript util type challenge: snake_case to camelCase and vice versa


I want to create a function in Typescript that traverses deeply a Plain object and replaces all snake_case keys with camelCase. Also I want to create a function that converts camelCase keys to snake_case deeply.

Implementing this in JavaScript is easy, but it's hard for me because I have to consider types.

How do I write this in TypeScript?

My JS version:

const keyToSnakeCase = obj => {
    if (Array.isArray(obj)) {
        return obj.map(el => keyToSnakeCase(el));
    }

    if (!isPlainObject(obj)) {
        return obj;
    }

    const newObj = {};

    Object.entries(obj).forEach(([key, value]) => {
        newObj[camelCase(key)] = keyToSnakeCase(value);
    });

    return newObj;
};

const keyToCamelCase = obj => {
    if (Array.isArray(obj)) {
        return obj.map(el => keyToCamelCase(el));
    }

    if (!isPlainObject(obj)) {
        return obj;
    }

    const newObj = {};

    Object.entries(obj).forEach(([key, value]) => {
        newObj[snakeCase(key)] = keyToCamelCase(value);
    });

    return newObj;
};

Solution

  • Thanks to @jcalz, I solved the problem.

    import isPlainObject from "lodash-es/isPlainObject";
    import camelCase from "lodash-es/camelCase";
    import snakeCase from "lodash-es/snakeCase";
    
    type SnakeToCamel<T extends string> = T extends `${infer F}_${infer R}`
      ? `${Lowercase<F>}${Capitalize<SnakeToCamel<R>>}`
      : T;
    
    type CamelToSnake<
      T extends string,
      A extends string = ""
    > = T extends `${infer F}${infer R}`
      ? CamelToSnake<R, `${A}${F extends Lowercase<F> ? F : `_${Lowercase<F>}`}`>
      : A;
    
    type DeepCamelKeys<T> = T extends readonly any[]
      ? { [I in keyof T]: DeepCamelKeys<T[I]> }
      : T extends object
      ? {
          [K in keyof T as K extends string ? SnakeToCamel<K> : K]: DeepCamelKeys<
            T[K]
          >;
        }
      : T;
    
    type DeepSnakeKeys<T> = T extends readonly any[]
      ? { [I in keyof T]: DeepSnakeKeys<T[I]> }
      : T extends object
      ? {
          [K in keyof T as K extends string ? CamelToSnake<K> : K]: DeepSnakeKeys<
            T[K]
          >;
        }
      : T;
    
    function keyToSnakeCase<T>(obj: T): DeepSnakeKeys<T>;
    function keyToSnakeCase(obj: any) {
      if (Array.isArray(obj)) {
        return obj.map((el) => keyToSnakeCase(el));
      }
    
      if (!isPlainObject(obj)) {
        return obj;
      }
    
      const newObj: any = {};
    
      Object.entries(obj).forEach(([key, value]) => {
        newObj[camelCase(key)] = keyToSnakeCase(value);
      });
    
      return newObj;
    }
    
    function keyToCamelCase<T>(obj: T): DeepCamelKeys<T>;
    function keyToCamelCase(obj: any) {
      if (Array.isArray(obj)) {
        return obj.map((el) => keyToCamelCase(el));
      }
    
      if (!isPlainObject(obj)) {
        return obj;
      }
    
      const newObj: any = {};
    
      Object.entries(obj).forEach(([key, value]) => {
        newObj[snakeCase(key)] = keyToCamelCase(value);
      });
    
      return newObj;
    }