Mapping type keys to camelCase in Typescript: a guide

As someone who is relatively new to typescript, I am eager to learn how to create a mapped type that converts keys from one type to another. Specifically, if I have a type where all the keys are written in snake case, how can I create a new type with camel-cased keys?

I was contemplating a solution like the following:

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

type MyCamelCaseType = CamelCase<SnakeCaseType>;

Unfortunately, TypeScript seems to be rejecting this approach. How can I go about transforming the keys of an existing type to generate a new type with this desired structure? Any guidance would be greatly appreciated.

Answer №1

The new enhancements to template literal types in Typescript 4.1 have provided a solution to a long-standing issue I've been facing. After some tinkering, I was able to come up with the following approach:

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 highly recommend checking out this article for more insights: https://dev.to/phenomnominal/i-need-to-learn-about-typescript-template-literal-types-51po and exploring some of these challenging Typescript exercises https://github.com/type-challenges/type-challenges to delve deeper into literal types.

Answer №2

If you come across an object that contains nested arrays, it's a good idea to add an extra layer of wrapping. This approach is inspired by the solution provided by Max Eisenhardt.

We can validate if the data is in Array format, and if it is, we can extract the object type within the current functionality.

Failing to do so will result in array functions being flagged as non-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]>
  }  

Answer №3

For those seeking the opposite transformation, here is an alternative solution to Max Eisenhardt's response:

type CapitalizedLetters = (
  '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 CamelCaseSeq<S extends string> = S extends `${infer P1}${infer P2}`
  ? P1 extends CapitalizedLetters
    ? `_${Lowercase<P1>}${CamelCaseSeq<P2>}`
    : `${P1}${CamelCaseSeq<P2>}`
  : Lowercase<S>;

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

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

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

Answer №4

After reviewing the responses from Max Eisenhardt and Hespen, I propose a solution that will work seamlessly even if your original type already has snake case keys. If not, the transformation will be as follows: 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]>
}  

Answer №5

Derived from Max Eisenhardt's response

This function will handle recursively any snake/dashes/specified delimiter present in the key of an object. It also accounts for unrolling array types to maintain the array structures.

/** Transform a string into camel-case based on a specific 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 keys of an object into camel-case using a specified 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];
}

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

/* ADDITIONAL TYPINGS FOR EASE OF USE */
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: ''
    }
}

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Get detailed coverage reports using Istanbul JS, Vue JS, Vue CLI, Cypress end-to-end tests, and Typescript, focusing on specific files for analysis

I have a VueJS app written in Typescript that I am testing with Cypress e2e tests. I wanted to set up coverage reports using Istanbul JS to track how much of my code is covered by the tests. The integration process seemed straightforward based on the docum ...

"Transferring a C# dictionary into a TypeScript Map: A step-by-step

What is the correct way to pass a C# dictionary into a TypeScript Map? [HttpGet("reportsUsage")] public IActionResult GetReportsUsage() { //var reportsUsage = _statService.GetReportsUsage(); IDictionary<int, int> te ...

What are some tips for leveraging Angular input signals in Storybook?

I am currently working on setting up Storybook 8.0.8 with Angular 17.3. I have been using the Angular input() signal in my components, but I've encountered an interesting issue where the args for the storybook stories also need the argument type to be ...

Error message: Typescript and Styled-Component conflict: GlobalStylesProps does not share any properties with type { theme?: DefaultTheme | undefined; }

I've encountered an issue while passing props inside GlobalStyles in the preview.js file of my storybook. The props successfully remove the background from the default theme, but I'm receiving an error from Typescript: The error message states " ...

Why am I unable to retrieve the property from the JSON file?

Below is the provided JSON data: data = { "company_name": "חברה לדוגמה", "audit_period_begin": "01/01/2021", "audit_period_end": "31/12/2021", "reports": [ ...

How can I incorporate a feature in my Angular application that allows users to switch between different view types, such as days, using JavaScript

Greetings, community! I am currently utilizing version 5 of the fullcalendar library from https://fullcalendar.io/ in my Angular 9 application. I have noticed that the calendar offers various options to change the view type as shown below: https://i.stac ...

Utilizing JSDoc for establishing an index signature on a class in ES6

When working with JSDoc, achieving the equivalent of Typescript's computed property names can be a challenge. In Typescript, you'd simply define it like this: class Test { [key: string]: whatever } This syntax allows you to access these comput ...

Steps for setting the keys of a map as a priority when initializing a state variable in Typescript for a React component

I am working with a state variable that connects a string username to a UserData object. Initially, the usernames are stored in the string array 'users'. Is there a method to assign the initial state of 'userDataMap' with the keys fro ...

Exploring the capabilities of google-diff-match-patch within the Angular framework

Seeking a way to incorporate the google diff/match/patch lib into an Angular application for displaying the variance between two texts. Here's how I plan on using it: public ContentHtml: SafeHtml; compare(text1: string, text2: string):void { var ...

Is there a way to turn off the warning overlay in a React application?

I’m currently using react-app-rewired and I am trying to figure out how to turn off the overlay that displays Typescript warnings whenever I compile. It seems like some warnings that are not caught by the VSCode Typescript checker pop up on this overlay ...

Ensuring TypeScript's strict null check on a field within an object that is part of an

When using TypeScript and checking for null on a nullable field inside an object array (where strictNullCheck is set to true), the compiler may still raise an error saying that 'Object is possibly undefined'. Here's an example: interface IA ...

Using Typescript, develop a function within an entity to verify the value of a property

In my Angular 7 app, I have an entity defined in my typescript file as follows: export class FeedbackType { id: number; name: String; } I am looking to create a function within this entity that checks the value of a property. For example: feedba ...

Unpacking and reassigning variables in Vue.js 3 using TypeScript

I am working with a component that has input parameters, and I am experimenting with using destructuring assignment on the properties object to reassign variables with different names: <script setup lang="ts"> const { modelValue: isSelected ...

A guide on incorporating JavaScript variables within a GraphQL-tag mutation

I'm having trouble consistently using javascript variables inside graphql-tag queries and mutations when setting up an apollo server. Here's a specific issue I've encountered: gql` mutation SetDeviceFirebaseToken { SetDeviceFirebaseTok ...

Angular's ng serve is experiencing issues with mark-compacts near the heap limit, leading to an unsuccessful allocation

Encountering an issue while running ng serve in my Angular project. However, ng build --prod seems to be working fine. <--- Last few GCs ---> [4916:00000276B1C57010] 588109 ms: Scavenge (reduce) 8180.7 (8204.3) -> 8180.6 (8205.1) MB, 3 ...

I'm looking for a way to implement a jQuery-style initialization pattern using TypeScript - how can I

My library utilizes a jQuery-like initialization pattern, along with some specific requirements for the types it should accept and return: function JQueryInitializer ( selector /*: string | INSTANCE_OF_JQUERY*/ ) { if ( selector.__jquery ) return select ...

Disabling the scrollbar within angular elements

Trying to remove the two scrollbars from this code, but so far unsuccessful. Attempted using overflow:hidden without success filet.component.html <mat-drawer-container class="example-container" autosize> <button type="button&qu ...

Guide for exporting and overloading function argument lists with tuples of varying lengths in Typescript

Unfortunately, I am facing an issue with Typescript 4.5.4 where the following overload with different tuples of varying lengths does not seem to work for me: export function f<T1> (t: [T1]) : any { ... } export function f<T1,T2> (t: [T1,T2 ...

Passing a callback to a third-party library resulted in an unexpected error

My React+TypeScript code utilizes a component from a third-party library. <ThirdPartyComponent onSelect={(value: any) => {...}} /> The eslint-typescript tool is flagging this as an error: Unexpected any. Specify a different type. eslint(@type ...

The Tauri JS API dialog and notification components are failing to function, resulting in a null return value

Currently, I am getting acquainted with the tauri framework by working on a small desktop application. While testing various tauri JS API modules, most of them have been functioning as expected except for the dialog and notification modules. Whenever I tes ...