Converting a generic object from snake_case to camelCase using TypeScript

Looking to create a function that can transform an object with snake case keys into one with camel case keys. How can this be achieved in TypeScript by typing the input object, but keeping the solution generic?

type InputType = {
  snake_case_key_1: number,
  snake_case_key_2: string,
  ...
}

function snakeToCamelCase(object: T): U {
  ...
}

What is the best approach to type T and U.

The goal is for U to have a highly specific type, ideally derived from T.

If T is like the example InputType, then U should be typed as:

{
  snakeCaseKey1: number,
  snakeCaseKey2: string,
  ...
}

Answer №1

Solution

Playground

This innovative solution leverages the power of template literal types in TypeScript 4.1 to convert snake_case to camelCase:

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)

This opens up possibilities for using key remapping in mapped types to create 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

Building on these types, conversion between various cases like Pascal case and kebab case is simplified with 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>>

To convert to kebab case, simply replace _ with - in the snake case type.

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

For lengthy strings that may cause this error, processing multiple sub-terms at once can manage type recursion effectively. Introducing SnakeToCamelCaseXXL for such scenarios:

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: With TS 4.5 increasing the type instantiation depth limit, advanced features like tail recursive evaluation are now available for complex cases.

Answer №3

After reviewing @ford04's input, a modified version of the SnakeCase function has been created to handle the first uppercase character and values that already contain underscores:

/**
 * This is a utility function that eliminates the initial underscore from a string
 */
type RemoveFirstUnderscore<S> = S extends `_${infer R}` ? R : S;

/**
 * Utility function that converts every uppercase character to snake case.
 * The initial unwanted underscore needs to be removed
 *
 * @example CamelToSnakeCase<'FooBarBaz'> // '_foo_bar_baz'
 */
type CamelToSnakeCase<S extends string> = S extends `${infer Head}${infer Rest}`
  ? `${Head extends '_'
      ? ''
      : Head extends Capitalize<Head>
      ? '_'
      : ''}${Lowercase<Head>}${CamelToSnakeCase<Rest>}`
  : S;

/**
 * Transforms a string from camelCase to snake_case format
 *
 * @example CamelToSnakeCase<'fooBarBaz'> // 'foo_bar_baz'
 */
type SnakeCase<S extends string> = RemoveFirstUnderscore<
  CamelToSnakeCase<S>
>;

type T31 = SnakeCase<"foo_bar">;
//   ^? foo_bar
type T32 = SnakeCase<"FooBar">;
//   ^? foo_bar
type T33 = SnakeCase<"fooBar">;
//   ^? foo_bar

Answer №4

To ridicule the approved solution and address a broader scenario, the following code demonstrates what is currently considered as the most effective approach. The function keysToCamelCase() utilizes a regular expression to split words based on types, converting them to camel case. The secondary function deepMapKeys() executes the core transformation logic. It supports specifying a maximum depth for conversion to camel case (or multiple depths, or using number to generate their union).

// Primary helper function.
function keysToCamelCase<T extends object, N extends number>(
    target: T,
    depth: N,
): CamelCaseProps<T, N> {
    return deepMapKeys(
        target,
        (key) => (typeof key == "string" ? toCamelCase(key) : key),
        depth,
    ) as any;
}

/**
 * Matches words using the pattern: [0-9]+|[A-Z]?[a-z]+|[A-Z]+(?![a-z])
 */

// Remaining code continues as it was originally provided...

Answer №5

If you're encountering issues with processing primitive arrays, you can make adjustments to the definition like this:

export type SnakeToCamelCaseNested<T> = T extends object
  ? T extends (infer U)[]
    ? U extends object
      ? { [K in keyof U as SnakeToCamelCase<K & string>]: SnakeToCamelCaseNested<U[K]> }[]
      : T
    : {
        [K in keyof T as SnakeToCamelCase<K & string>]: SnakeToCamelCaseNested<T[K]>;
      }
  : T;

Original version ():

const form: SnakeToCamelCaseNested<{my_tags: string[]}> = ...

function checkCase(data: {myTags: string[]}){ ... }

checkCase(form)
 

Property types for 'tags' are conflicting. Type '{ length: number; to_string: {}; to_locale_string: {}; pop: {}; push: {}; concat: {}; join: {}; reverse: {}; shift: {}; slice: {}; sort: {}; splice: {}; unshift: {}; index_of: {}; last_index_of: {}; every: {}; ... 18 more ...; find_last_index: {}; } | undefined' cannot be assigned to type 'string[]'

Answer №6

We regret to inform you that achieving such functionality is currently impossible with Typescript. The language does not offer native support for transforming or mapping type keys.

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

Instructions for enabling the touch slider feature in the Igx carousel component with Angular 6 or higher

Looking to enable the touch slider for Igx carousel using angular 6+? I am trying to implement the igx carousel for image sliding with reference from a stackblitz demo (https://stackblitz.com/edit/github-j6q6ad?file=src%2Fapp%2Fcarousel%2Fcarousel.compone ...

The Next JS project fails to compile when a hyperlink is sent to the Link component from an external source

I am encountering an issue with a Menu Item component that pulls its href and label from another component known as NavBar. The strange thing is that it works perfectly fine when running yarn dev, but fails to build. However, when I make a simple change to ...

Ways to display a component with different initial state when needed?

Within my application, I have a specific component that independently manages its state using the useState hook. However, I am encountering an issue where I need to conditionally render multiple instances of this same component: const PaymentScannerView: R ...

Accessing data from an API and showcasing information on a chart using Angular

I'm currently developing a dashboard application that requires me to showcase statistics and data extracted from my MongoDB in various types of charts and maps using Angular and Spring Boot. The issue I'm facing is that when attempting to consume ...

Is there a way to prevent nesting subscriptions in rxjs?

Currently, I am working with a code that contains nested subscribes: .subscribe((data) => { const { game, prizes } = data; this.ticketService.setListOfTickets(game.tickets); this.ticketService.getListOfTickets() .subscribe((data: any) => { ...

What steps can I take to troubleshoot why a pop-up window will appear in web Outlook but not in the 2016 version

While my dialog opens correctly in the Office web app, it only displays a loading indicator and shows "working on your request" in Office 2016. I've attempted to add a task pane, which successfully works and allows me to accept the HTTPS certificate o ...

What is the most efficient way to update data multiple times by mapping over an array of keys in a react hook?

My question might not be articulated correctly. I'm facing an issue with dynamically translating my webpage using Microsoft's Cognitive Services Translator. I created a react hook for the translator, which works well when I need to translate a si ...

Facing a 'No provider for' error in my Angular 2.0.0 application

I recently developed a service called SecurityService to handle authentication. Check out the code for this service below: import { Injectable } from '@angular/core'; @Injectable() export class SecurityService { items: any[]; construct ...

Issue: Oops! The digital envelope routines are not supported in Angular while attempting to run the project

I encountered an error when running the command below: ng s The error message is as follows: Error: error:0308010C:digital envelope routines::unsupportedat new Hash (node:internal/crypto/hash:68:19)at Object.createHash (node:crypto:138:10)at BulkUpdateDe ...

Guide on linking action observables to emit values in sync before emitting a final value

If you're familiar with Redux-Observable, I have a method that accepts an observable as input and returns another observable with the following type signature: function (action$: Observable<Action>): Observable<Action>; When this method r ...

Union types discriminate cases within an array

Creating a union type from a string array: const categories = [ 'Category A', 'Category B' ] as const type myCategory = typeof categories[number] myCategory is now 'Category A' | 'Category B' Now, the goal is ...

Dynamically setting the IMG SRC attribute with the base64 result of a FileReader for file input

Looking for a little guidance on something new, I'll keep it brief because I'm sure it's just some small detail I'm overlooking... Starting with an image like this, where currentImage is the initial existing image path; <img src="{ ...

Navigating through JSON object using Angular 2's ngFor iterator

I want to test the front end with some dummy JSON before I write a service to get real JSON data. What is the correct way to iterate through JSON using ngFor? In my component.ts file (ngOnInit()), I tried the following code with a simple interface: var js ...

Instance property value driven class property type guard

Is it possible to create a class example that can determine the config type based on the value of animalType instance: enum Animal { BIRD = 'bird', DOG = 'dog', } type Base = { id: number } // Object example type Smth = Base & ...

Exploring the Concept of Dependency Injection in Angular 2

Below is a code snippet showcasing Angular 2/Typescript integration: @Component({ ... : ... providers: [MyService] }) export class MyComponent{ constructor(private _myService : MyService){ } someFunction(){ this._mySer ...

Expanding the interactive capabilities of a stateful component with connections

I am looking to design a foundational Redux component with its own state and properties. As I extend it in a generic fashion, I aim to combine the properties and state of the extended object with the base. It is crucial for this component to be linked with ...

The search is fruitless for a distinguishable entity '[object Object]' falling under the category of 'object'. In Angular 8, NgFor exclusively allows binding to Iterables like Arrays

My goal is to display a list of users using the ngFor directive. However, when I attempt to do this, the console displays an error message: Error ERROR Error: Cannot find a differ supporting object '[object Object]' of type 'object' ...

Tips for configuring the _document.tsx file in Next.js for optimal performance

I found most of the code for this project in the official documentation example on utilizing styled-components: https://github.com/vercel/next.js/blob/canary/examples/with-styled-components/pages/_document.js However, the example was written in .js and I ...

Encountering an unanticipated DOMException after transitioning to Angular 13

My Angular project is utilizing Bootstrap 4.6.2. One of the components features a table with ngb-accordion, which was functioning properly until I upgraded the project to Angular 13. Upon accessing the page containing the accordion in Angular 13, I encount ...

TypeScript is throwing an error about a missing property, even though it has been defined

Within the PianoMK1Props component, there is a prop known as recording which accepts an object with specific properties. The structure of this object is defined like so(the state variable): const [recording, setRecording] = useState({ mode: "REC ...