Tips on streamlining two similar TypeScript interfaces with distinct key names

Presented here are two different formats for the same interface: a JSON format with keys separated by low dash, and a JavaScript camelCase format:

JSON format:

interface MyJsonInterface {
  key_one: string;
  key_two: number;
}

interface MyInterface {
  keyOne: string;
  keyTwo: number;
}

In order to avoid duplications, I am unsure of the correct approach. I have reviewed this question, but the provided answer did not meet my requirements as I do not want the same keys to be accessible in both interfaces.

Is there an alternative solution available?

Answer №1

Breaking down this task into smaller subtasks is essential. To begin with, you will need to create a utility type that can convert snake_case to camelCase. Let's concentrate on this aspect first.

Take a look at the following:

type Separator = '_'
type Convert<Str extends string, Acc extends string = ''> =
  // Verify if Str follows the pattern string_string
  (Str extends `${infer Head}${Separator}${infer Tail}`
    // If yes, determine whether it is the initial call or not, as we don't want to capitalize part of the string
    ? (Acc extends ''
      // As Acc is empty, this is the starting call and the first part should not be capitalized
      ? Convert<Tail, `${Acc}${Head}`>
      // This is not the starting call, so Head should be capitalized
      : Convert<Tail, `${Acc}${Capitalize<Head>}`>)
    // As Str does not match the pattern, this is the final call
    : `${Acc}${Capitalize<Str>}`)

Now, we can iterate through the interface and replace each key with its converted version:

type Builder<T> = {
  [Prop in keyof T as Convert<Prop & string>]: T[Prop]
}

// Transformed object keys:
// {
//     oneTwoThreeFourthFiveSixSevenEightNineTen: "hello";
// }
type Result = Builder<{
  one_two_three_fourth_five_six_seven_eight_nine_ten: 'hello'
}>

Here's a Playground link with the complete code

To reverse the conversion process:

type Separator = '_'

type IsChar<Char extends string> = Uppercase<Char> extends Lowercase<Char> ? false : true;

type IsCapitalized<Char extends string> =
  IsChar<Char> extends true
  ? Uppercase<Char> extends Char
  ? true
  : false
  : false

type Replace<Char extends string> =
  IsCapitalized<Char> extends true
  ? `${Separator}${Lowercase<Char>}`
  : Char

type Result2 = Replace<'A'>

type CamelToSnake<
  Str extends string,
  Acc extends string = ''
  > =
  Str extends `${infer Char}${infer Rest}` ? CamelToSnake<Rest, `${Acc}${Replace<Char>}`> : Acc

// Converted string: "foo_bar_baz"
type Result = CamelToSnake<'fooBarBaz'>

Access the Playground link here

Answer №2

Here is a solution that will work for you.

interface MyJsonInterface {
  key_one: string;
  key_two: number;
  key_three_other: number;
  key_four_with_another: number;
}

type PropMapping<T> =
  T extends `${infer ST}_${infer ND}_${infer RD}_${infer TH}`
  ? `${ST}${Capitalize<ND>}${Capitalize<RD>}${Capitalize<TH>}`
  : T extends `${infer ST}_${infer ND}_${infer RD}`
  ? `${ST}${Capitalize<ND>}${Capitalize<RD>}`
  : T extends `${infer ST}_${infer ND}`
  ? `${ST}${Capitalize<ND>}`
  : never

type MyInterface = {
  [K in keyof MyJsonInterface as PropMapping<K>]: MyJsonInterface[K]
}

https://i.sstatic.net/zqiJR.png

Answer №3

Utilizing the concepts from the user-friendly and understandable response mentioned above by @lepsch, I have crafted a version capable of managing any number of lodashs or subscripts.

Note: It appears that there is a limit of up to 1000 lodashs due to TypeScript's built-in tail-recursion depth restriction since version 4.5. Testing with 1000 showed successful results. Strangely, using 1001 lodashs did not trigger an error indicating 'possibly infinite recursion depth,' but instead caused the key to disappear from MyInterface.

For easier reference, here is a link to the relevant Typescript Playground.

interface MyJsonInterface {
  key_one: string;
  key_two: number;
  key_three_other: number;
  key_four_with_another: number;
  key_with_many_more_lodashs_bla_bla_bla_bla_bla: boolean;
  // quirk: keys starting with _ will be turned into upper case (-> HelloStuff)
  __hello__stuff: number
}

type SubstringUntilLodash<T> = T extends `${infer U}_${infer P}` ? U : never; 
type SubstringAfterLodash<T> = T extends `${infer U}_${infer P}` ? P : never; 
type ContainsLodash<T> = SubstringAfterLodash<T> extends '' ? false : true;

type PropMappingRecursive<T extends string, Original extends string = T, Processed extends string = "", Result extends string = ""> =
  // recursion anchor 1: if no lodash contained in T
  ContainsLodash<T> extends false ? 
  // then return T if it was the original string (handles that the first latter is small), else the previous result plus capitalized T.
  T extends Original ? T : `${Result}${Capitalize<T>}` :
  // Making sure the first substring starts with a small letter, and dorecursive call
  Processed extends '' ? PropMappingRecursive<SubstringAfterLodash<T>, Original, `${Processed}_${SubstringUntilLodash<T>}`, `${SubstringUntilLodash<T>}`> :
  // else capitalize and do recursive call
  PropMappingRecursive<SubstringAfterLodash<T>, Original, `${Processed}_${SubstringUntilLodash<T>}`, `${Result}${Capitalize<SubstringUntilLodash<T>>}`>

type MyInterface = {
  [K in keyof MyJsonInterface as PropMappingRecursive<K>]: MyJsonInterface[K]
}

https://i.sstatic.net/ZgK2v.png

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

The 'BaseResponse<IavailableParameters[]>' type does not contain the properties 'length', 'pop', etc, which are expected to be present in the 'IavailableParameters[]' type

After making a get call to my API and receiving a list of objects, I save that data to a property in my DataService for use across components. Here is the code snippet from my component that calls the service: getAvailableParameters() { this.verifi ...

Unlock the power of Env variables on both server and client components with Next.js! Learn how to seamlessly integrate these

In my Next.js app directory, I am facing the need to send emails using Nodemailer, which requires server-side components due to restrictions on client-side sending. Additionally, I am utilizing TypeScript in this project and encountering challenges when tr ...

"Learn the steps to seamlessly add text at the current cursor position with the angular-editor tool

How can I display the selected value from a dropdown in a text box at the current cursor position? I am currently using the following code: enter code selectChangeHandler(event: any) { this.selectedID = event.target.value; // console.log("this.selecte ...

Error in Docker: Unable to resolve due to sender error: context has been terminated

After attempting to build my docker image for the project in VS Code terminal, I ran into an error. What are some possible reasons for this issue? Along with this question, I have also shared a screenshot of the error logs. ERROR: failed to solve: error ...

Angular2 had a delay in processing the 'mouse wheel' input event for x milliseconds because the main thread was occupied

Currently, I am working with Angular2 (r.2.0.0) along with typescript (v.1.8.10). I have encountered an issue where Chrome does not respond while scrolling and displays a warning message stating: "Handling of 'mouse wheel' input event was delayed ...

How to retrieve a random element from an array within a for loop using Angular 2

I'm in the process of developing a soundboard that will play a random sound each time a button is clicked. To achieve this, I have created an array within a for loop to extract the links to mp3 files (filename), and when a user clicks the button, the ...

React canvas losing its WebGL context

What is the best practice for implementing a webglcontextlost event handler for React canvas components? class CanvasComponent extends React.Component { componentDidMount() { const canvasDOMNode = this.refs.canvas.getDOMNode(); DrawMod ...

Merge the values of an object's key with commas

I'm dealing with an array of objects that looks like this: let modifiers = [ {name: "House Fries", price: "2.00"}, {name: "Baked Potato", price: "2.50"}, {name: "Grits", price: "1.50"}, {name: "Nothing on Side", price: "0.00"} ] My goal is to con ...

How to transfer a parameter in Angular 2

I am currently facing a challenge in passing a value from the HTML file to my component and then incorporating it into my JSON feed URL. While I have successfully transferred the value to the component and displayed it in the HTML file, I am struggling to ...

Exceeded maximum stack size error encountered in Ionic 2 Tab bar functionality

When I attempt to incorporate a tab bar into my application, an error message saying "Maximum call stack size exceeded" is displayed Profile.html <ion-tabs> <ion-tab tabIcon="water" tabTitle="Water" ></ion-tab> <ion-tab tabI ...

Utilizing a syntax highlighter in combination with tsx React markdown allows for cleaner

I'm currently looking at the React Markdown library on Github and attempting to incorporate SyntaxHighlighter into my markdown code snippets. When I try to implement the example code within a function used for rendering posts, I encounter the error de ...

TS2688 Error: Type definition file for 'keyv' is missing

The automated code build process failed last night, even though I did not make any changes related to NPM libraries. The error message I received was: ERROR TS2688: Cannot find type definition file for 'keyv'. The file is in the program because: ...

The attribute 'y' is not found within the data type 'number'

Currently working on a project using Vue.js, Quasar, and TypeScript. However, encountering errors that state the following: Property 'y' does not exist on type 'number | { x: number[]; y: number[]; }'. Property 'y' does not ...

Trouble arises when attempting to transfer cookies between server in Fastify and application in Svelte Kit

In the process of developing a web application, I am utilizing Fastify for the backend server and Svelte Kit for the frontend. My current challenge lies in sending cookies from the server to the client effectively. Despite configuring Fastify with the @fas ...

Use an input of map<string,string> when passing to an angular component

Trying to pass an input value for a component reuse, but facing the challenge of having to use a "hardcoded" map of strings. Uncertain about how to effectively pass this information: <continue-p [additionalInfo]="{ "myString": "str ...

Is TypeScript failing to enforce generic constraints?

There is an interface defined as: export default interface Cacheable { } and then another one that extends it: import Cacheable from "./cacheable.js"; export default interface Coin extends Cacheable{ id: string; // bitcoin symbol: stri ...

Guide to retrieving the previous URL in Angular 2 using Observables

Can someone help me retrieve my previous URL? Below is the code snippet I am working with: prev2() { Promise.resolve(this.router.events.filter(event => event instanceof NavigationEnd)). then(function(v){ console.log('Previous ' ...

Why aren't the child elements in my React / Framer Motion animation staggered as expected?

In my finance application, I am creating a balance overview feature. To display the content, I pass props into a single <BalanceEntry> component and then map all entries onto the page. With Framer Motion, my goal is to animate each rendered <Bala ...

Troubleshooting Angular 6: Issues with Route Guards not functioning as expected

Striving to enhance frontend security by restricting access to specific IDs. The goal is to redirect anyone trying to access routes other than /login/:id to a page-not-found error message if not already logged in, but encountering some issues. Below are t ...

Shattered raw emotion

Does anyone have any insight on how to resolve this error? I've hit a roadblock trying to figure out the issue in the message below. Here is the snippet of code: :label="` ${$t('cadastros.clientes.edit.status')}: ${cliente.status === ...