23

I'm new to typescript but I want to create a mapped type that converts keys from another type. Specifically, say I have a type where all the keys are snake-cased, how can I create a type where they are all camel-cased?

I thought I could do something like

type CamelCase<T> = {
  [_.camelCase(P in keyof T)]: T[P];
}

type MyCamelCaseType = CamelCase<SnakeCaseType>;

But TS doesn't like this. How can I transform the keys of an existing type to create a new type in this way?

2

5 Answers 5

51

In Typescript 4.1, template literal types got quite an upgrade. This is a problem I have wanting to be solved for a while and with a bit of tweaking managed to come up with this:

type CamelCase<S extends string> = S extends `${infer P1}_${infer P2}${infer P3}`
  ? `${Lowercase<P1>}${Uppercase<P2>}${CamelCase<P3>}`
  : Lowercase<S>

type KeysToCamelCase<T> = {
    [K in keyof T as CamelCase<string &K>]: T[K] extends {} ? KeysToCamelCase<T[K]> : T[K]
}


interface SnakeCase {
    bar_value: string;
    baz_value: {
        blah_test: number;
    }
}

const transformed: KeysToCamelCase<SnakeCase> = {
    bazValue: {
        blahTest: 2
    },
    barValue: 'test'
}

I can recommend reading: https://dev.to/phenomnominal/i-need-to-learn-about-typescript-template-literal-types-51po and also trying some of these Typescript challenges https://github.com/type-challenges/type-challenges to learn more about these literal types.

Sign up to request clarification or add additional context in comments.

8 Comments

How would this work for arrays? If the SnakeCase contains an array, the final generated type seems to throw the following error, after trying to use an array function on it. TS2349: This expression is not callable.Type 'KeysToCamelCase<ArrayType>' has no call signatures. For example if this is the object you want to transform: ``` { bar_value: string; baz_value: [{ blah_test: number; }] } ``` Then the transformed bazValue.map(() => {}) will throw an error. Any idea how this can be solved?
I think, Lowercase<S> isn't nessessary, but its existence makes imposible to apply CamelCase to keys that are already in camel case
awosome! Is any possible to do this opposite? snakecase to camelcase?
Thank you! This works like a charm. I tried to understand the code but there's one thing I couldn't figure out. Would you please let me know why we need to use CamelCase<string & K>? What does the & K do in this part?
Actually, it's more like K & string. That's because keyof T type is not only a string type, but rather string | number | symbol. So we need to limit type of K only to string type, because CamelCase type expects only strings.
|
11

In case you have an object with nested arrays in it, you can wrap it a bit more. Based on Max Eisenhardts answer.

We can check on an Array type, and if so, take the object type of that Array in the already existing functionality.

If we don't do this, array functions will be marked as not callable.

 type CamelCase<S extends string> = S extends `${infer P1}_${infer P2}${infer P3}`
    ? `${Lowercase<P1>}${Uppercase<P2>}${CamelCase<P3>}`
    : Lowercase<S>

  type ObjectToCamel<T> = {
    [K in keyof T as CamelCase<string &K>]: T[K] extends Record<string, any> ? KeysToCamelCase<T[K]> : T[K]
  }

  type KeysToCamelCase<T> = {
    [K in keyof T as CamelCase<string &K>]: T[K] extends Array<any> ? KeysToCamelCase<T[K][number]>[] : ObjectToCamel<T[K]>
  }  

4 Comments

KeysCamelArray is not defined
Made a typo, it's fixed now.
To save an array elements type, you should pass it T[K] extends Record<string, any>
The ObjectToCamel type is losing array prototype functions. To fix this, use @MikeKokadii 's answer, where you switch Record<string, any> to Record<string, K>
1

And the reverse transformation in addition to Max Eisenhardt's answer for those who asked:

type UpperCaseLetters = (
  'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H' | 'I' | 'J' | 'K' | 'L' |
  'M' | 'N' | 'O' | 'P' | 'Q' | 'R' | 'S' | 'T' | 'U' | 'V' | 'W' | 'X' |
  'Y' | 'Z' | '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
)

type SnakeCaseSeq<S extends string> = S extends `${infer P1}${infer P2}`
  ? P1 extends UpperCaseLetters
    ? `_${Lowercase<P1>}${SnakeCaseSeq<P2>}`
    : `${P1}${SnakeCaseSeq<P2>}`
  : Lowercase<S>;

export type SnakeCase<S extends string> = S extends `${infer P1}${infer P2}`
  ? `${Lowercase<P1>}${SnakeCaseSeq<P2>}`
  : Lowercase<S>;

type ObjectToSnakeCase<T> = {
  [K in keyof T as SnakeCase<string &K>]: T[K] extends Record<string, any>
    ? KeysToSnakeCase<T[K]>
    : T[K];
};

export type KeysToSnakeCase<T> = {
  [K in keyof T as SnakeCase<string &K>]: T[K] extends Array<any>
    ? KeysToSnakeCase<T[K][number]>[]
    : ObjectToSnakeCase<T[K]>;
};

1 Comment

Isn't P1 extends UpperCaseLetters equivalent to P1 extends Uppercase<P1>?
0

Adapted from Max Eisenhardt's answer

This will recursively handle as many snake/dashes/some delimiter that are present in an object's key. Accounts for unrolling array types to maintain array structures.

/** Converts a string to camel-case based on some delimiter */
export type CamelCaseFrom<S extends string, Delimiter extends string> = CamelCaseFromHelper<S, Delimiter>;

type CamelCaseFromHelper<S extends string, Delimiter extends string, NotFirstToken extends boolean = false> =
    NotFirstToken extends true
        ? S extends `${infer P1}${Delimiter}${infer P2}`
            ? `${Capitalize<P1>}${CamelCaseFromHelper<P2, Delimiter, true>}`
            : `${Capitalize<S>}`
        : S extends `${infer P1}${Delimiter}${infer P2}`
            ? `${Lowercase<P1>}${CamelCaseFromHelper<P2, Delimiter, true>}`
            : `${Lowercase<S>}`;

/** Convert an object's keys to camel-case based on some delimiter */
export type KeysToCamelCase<T, Delimiter extends string> = {
    [K in keyof T as CamelCaseFrom<string &K, Delimiter>]:
        T[K] extends Array<infer ArrayElement>
            ? KeysToCamelCaseForArrayElement<ArrayElement, Delimiter>
            : T[K] extends {}
                ? KeysToCamelCase<T[K], Delimiter>
                : T[K];
}

/** Handles selecting keys from nested arrays */
type KeysToCamelCaseForArrayElement<AElement, Delimiter extends string> =
    AElement extends Array<infer BElement>
        ? Array<KeysToCamelCaseForArrayElement<BElement, Delimiter>>
        : Array<KeysToCamelCase<AElement, Delimiter>>;

/* CONVENIENCE TYPINGS */
export type CamelCaseFromKebabCase<S extends string> = CamelCaseFrom<S, '-'>;
export type CamelCaseFromSnakeCase<S extends string> = CamelCaseFrom<S, '_'>;

Example 1

interface MyKebabCaseConfig {
    'foo': string;
    'foo-bar-qaz-pom-dee': string;
    'nested-object': {
        'first-name': string
        'date-of-birth': string;
        'another-one': {
            'aaa-bbb-ccc': string;
        };
        'some-array': string[];
    };
    'array-with-deep-object': SomeDeepObject[][];
}

interface SomeDeepObject {
    'aaa-bbb-ccc': string;
    'deep-array-of-strings': string[][][][];
    'deep-array-of-another-object': {
        'ddd-eee-fff': string;
    }[][][]
}

const transformed: KeysToCamelCase<MyKebabCaseConfig, '-'> = {
    foo: '',
    fooBarQazPomDee: '',
    nestedObject: {
        firstName: '',
        dateOfBirth: '',
        anotherOne: {
            aaaBbbCcc: ''
        },
        someArray: []
    },
    arrayWithDeepObject: [
        [
            {
                aaaBbbCcc: '',
                deepArrayOfStrings: [ [ [ [ '' ] ] ] ],
                deepArrayOfAnotherObject: [ [ [ { dddEeeFff: '' } ] ] ]
            }
        ]
    ]
}

Example 2

interface BangCaseObject {
    'aaa!bbb!ccc': string;
    'eee!ddd': {
        'fff!ggg': string;
    }
}

const x: KeysToCamelCase<BangCaseObject, '!'> = {
    aaaBbbCcc: '',
    eeeDdd: {
        fffGgg: ''
    }
}

Comments

-1

Based on Max Eisenhardt and Hespen answers, I suggest the solution that will worked even if you have snake case keys in your original type already. Otherwise you will have next transformation: camelCase -> camelcase

type CamelCase<S extends string> = S extends `${infer P1}_${infer P2}${infer P3}`
    ? `${Lowercase<P1>}${Uppercase<P2>}${CamelCase<P3>}`
    : Lowercase<S>

type ObjectToCamel<T> = {
    [K in keyof T as CamelCase<string &K>]: T[K] extends Record<string, K> ? KeysToCamelCase<T[K]> : T[K]
}

type KeysToCamelCase<T> = {
    [K in keyof T as K extends `${infer P1}_${infer P2}`
        ? CamelCase<string &K>
        : K
    ]: T[K] extends Array<K>
        ? KeysToCamelCase<T[K][number]>[]
        : ObjectToCamel<T[K]>
}  

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.