Determining the type of object derived from a complex assembly of nested objects and components

Consider the structure provided below:

const myForm = {
  widget: "col",
  children: [
    {
      widget: 'row',
      children: [
        {
          widget: 'text',
          prop: 'name'
        },
        {
          widget: 'number',
          prop: 'age'
        }
      ]
    },
    {
      widget: 'text',
      prop: 'location',
      optional: true
    }
  ]
} as const

type MyForm = Infer<typeof myForm>

The goal is to deduce the following type:

{
  name: string
  age: number
  location?: string
}

Progress has been made in traversing the object tree and inferring types for each widget. However, there is a roadblock when it comes to constructing the final object. Access this playground link to see what has been accomplished with regards to the MyForm type.

Answer №1

Let's kick things off with the basics. Eventually, we will have to translate from the string representation "text" to the type string and other types. In your project space, you opted for a conditional type to achieve this. While it works, using a straightforward map approach can enhance readability and scalability (plus, it improves performance too ^^).

type MappingType = {
  "text": string
  "number": number
}

I've also defined a type called DataElement for the provided data structure.

type DataElement = { 
  widget: string, 
  children?: readonly DataElement[], 
  property?: string, 
  optional?: boolean 
}

This setup simplifies accessing properties later by indexing the generic type T. By utilizing this type as a constraint for T, we can perform actions like

T["property"]</code without the need to explicitly <em>validate</em> if the property exists.</p>
<p>Now onto the more complex part. We understand that the resulting type is a flat object.</p>
<pre><code>type MyData = {
    name: string;
    age: number;
    location?: string | undefined;
}

We are aware that such object types can be built using mapped types and that we require a union for mapping. Ideally, we could create a helper type that generates a union from a given type which would resemble this:

{
    widget: "text";
    property: "name";
} | {
    widget: "number";
    property: "age";
} | {
    widget: "text";
    property: "location";
    optional: true;
}

This union should encompass all necessary information about each resulting property.

To construct the union, I developed the recursive type known as RetrievePropertiesRecursive.

type RetrievePropertiesRecursive<T extends DataElement> =
  | (T["property"] extends string ? T : never)
  | (T["children"] extends readonly any[] 
      ? T["children"][number] extends infer U 
        ? U extends DataElement
          ? RetrievePropertiesRecursive<U>
          : never
        : never
      : never
    )

RetrievePropertiesRecursive initiates at the root of the object type and begins constructing the union by conducting two checks. Firstly, it verifies if T["property"] is defined. If so, we include T in the union. Next, it examines whether T["children"] is established. In such cases, we transform the tuple of children into a union of child elements by indexing T["children"] with number. These union elements are now usable in the recursive call to RetrievePropertiesRecursive.

Here lies a rather "hidden" TypeScript mechanism. By storing T["children"][number] within U and confirming if U extends DataElement, we are distributing the union elements across RetrievePropertiesRecursive.

Hence, instead of having

RetrievePropertiesRecursive<E_1 | E_2 | E_3>
(where E_N represents a union element), we distribute over RetrievePropertiesRecursive, resulting in
RetrievePropertiesRecursive<E_1> | RetrievePropertiesRecursive<E_2> | RetrievePropertiesRecursive<E_3>
.

Finally, onto the last segment.

type Deduce<T extends DataElement> = (RetrievePropertiesRecursive<T> extends infer U ? ({
  [K in U as K extends { optional: true } 
    ? never 
    : K["property" & keyof K] & string
  ]: MappingType[K["widget" & keyof K] & keyof MappingType]
} & {
  [K in U as K extends { optional: true } 
    ? K["property" & keyof K] & string 
    : never
  ]?: MappingType[K["widget" & keyof K] & keyof MappingType]
}) : never) extends infer O ? { [K in keyof O]: O[K] } : never

We invoke

RetrievePropertiesRecursive<T>
and store the outcome in U. Since properties where optional is true should be optional, we must form two separate mapped types, one using the ? notation and then intersect them. For each respective mapped type, we sift through the union elements where optional is true or false respectively. To fetch the property name, we operate K["property"]K["widget"]

Let's start with the simple stuff. At some point, we will need to map from the string representation "text" to the type string and other types. In your playground, you used a conditional type for this. That works, but using a simple map makes it easier to understand and expandable (and the performance is better too ^^).

type WidgetToType = {
  "text": string
  "number": number
}

I also define a type FormElement for the given data structure.

type FormElement = { 
  widget: string, 
  children?: readonly FormElement[], 
  prop?: string, 
  optional?: boolean 
}

This will make it easier to access properties by indexing the generic type T later. We can use the type as a constraint for T to do things like T["prop"] without needing to check if the property exists.

Now to the harder part. We know the resulting type is a flat object.

type MyForm = {
    name: string;
    age: number;
    location?: string | undefined;
}

We also know that we can construct such object types with mapped types and that we need a union for mapping. So ideally we can create a helper type which will create a union from a given type which will look like this:

{
    widget: "text";
    prop: "name";
} | {
    widget: "number";
    prop: "age";
} | {
    widget: "text";
    prop: "location";
    optional: true;
}

This union should contain all the needed information about each resulting property.

To create the union, I wrote the recursive type GetPropsDeeply.

type GetPropsDeeply<T extends FormElement> =
  | (T["prop"] extends string ? T : never)
  | (T["children"] extends readonly any[] 
      ? T["children"][number] extends infer U 
        ? U extends FormElement
          ? GetPropsDeeply<U>
          : never
        : never
      : never
    )

GetPropsDeeply starts at the root of the object type and begins building the union by performing two checks. First, it checks if T["prop"] is set. If so, we can add T to the union. Then it checks if T["children"] is set. If that is the case, we convert the children tuple to a union of children elements by indexing T["children"] with number. We can now use these union elements in the recursive call to GetPropsDeeply.

Here is also a "hidden" TypeScript mechanic. By storing T["children"][number] inside U and checking if U extends FormElement, we are distributing the union elements over GetPropsDeeply.

So instead of having

GetPropsDeeply<E_1 | E_2 | E_3>
(where E_N is a union element), we distribute over GetPropsDeeply resulting in
GetPropsDeeply<E_1> | GetPropsDeeply<E_2> | GetPropsDeeply<E_3>
.

Now to the last part.

type Infer<T extends FormElement> = (GetPropsDeeply<T> extends infer U ? ({
  [K in U as K extends { optional: true } 
    ? never 
    : K["prop" & keyof K] & string
  ]: WidgetToType[K["widget" & keyof K] & keyof WidgetToType]
} & {
  [K in U as K extends { optional: true } 
    ? K["prop" & keyof K] & string 
    : never
  ]?: WidgetToType[K["widget" & keyof K] & keyof WidgetToType]
}) : never) extends infer O ? { [K in keyof O]: O[K] } : never

We call GetPropsDeeply<T> and store the result in U. Since properties where optional is true are supposed to be optional, we need to construct two seperate mapped types where one is using the ? notation and intersect them. For the respective mapped type, we filter out the union elements where optional is true or false respectively. To get the property name, we do K["prop"] and we also use our mapping here to get the corresponding type of K["widget"].

Playground

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

"In Typescript, receiving the error message "Attempting to call an expression that is not callable" can be resolved

I am in the process of creating a function that matches React's useState signature: declare function useState<S>( initialState: S | (() => S), ): [S, React.Dispatch<React.SetStateAction<S>>]; Below is an excerpt from the functi ...

Accept an empty string as the defaultValue, but disallow it during validation using Zod, react-hook-form, and Material UI

Currently, I am working with material ui components alongside react-hook-form and zod validation in my project. One of the challenges I encountered is with a select field for a bloodType: const bloodTypes = [ "A+", "A-", "B+", ...

Creating customized object mappings in Typescript

In my current Angular project, I am working on mapping the response from the following code snippet: return this.http.get(this.url) .toPromise() .then(response => response as IValueSetDictionary[]) .catch(this.handleError); The respon ...

Typescript mistakenly labels express application types

Trying to configure node with typescript for the first time by following a tutorial. The code snippet below is causing the app.listen function to suggest incorrectly (refer to image). import express from 'express'; const app = express(); app.li ...

Retrieve the value of an object without relying on hardcoded index values in TypeScript

I am working with an object structure retrieved from an API response. I need to extract various attributes from the data, which is nested within another object. Can someone assist me in achieving this in a cleaner way without relying on hardcoded indices? ...

What is the best way to filter specific data types when using ngFor in Angular?

As I loop through the array named "fruits," which contains objects of type "FruitService" that I created, I want to display each element. However, when I delete them (I know it's strange, no database involved), they turn into type "undefined" and star ...

@ngrx effects ensure switchmap does not stop on error

Throughout the sign up process, I make 3 http calls: signing up with an auth provider, creating an account within the API, and then logging in. If the signup with the auth provider fails (e.g. due to an existing account), the process still tries to create ...

Merging RXJS observable outputs into a single result

In my database, I have two nodes set up: users: {user1: {uid: 'user1', name: "John"}, user2: {uid: 'user2', name: "Mario"}} homework: {user1: {homeworkAnswer: "Sample answer"}} Some users may or may not have homework assigned to them. ...

The Optimal Method for Calculating an SDF for a Triangle Mesh

https://i.sstatic.net/L7Zmz.jpg Hey there! For the past month, I've been diligently researching various sources in hopes of finding a solution to my unique problem. Here's the issue I'm grappling with: In the context of a mesh represented ...

Strategies for handling timeouts in TypeScript testing

When I attempt to use the calculator method to trigger the email() method, there seems to be an issue with the elements not rendering properly on the page. After waiting for a significant amount of time, a timeout error is thrown. Has anyone encountered th ...

What is the best way to store a gridster-item in the database when it is resized or changed using a static function

Following some resize and drag actions on my dashboard, I aim to store the updated size and position of my altered widget in my MongoDB database. Even though the gridster library offers the ability to respond to draggable and resizable events, these events ...

Failed to interpret ratio in Uniswap V3 SwapRouter.swapCallParameters

Following this guide (code), I am attempting a basic trade operation. I have made modifications to some functions in the code to accommodate parameters such as: Token in Token out Amount in The following is my customized createTrade function: export asyn ...

Utilizing the return value of a MockService in a Jasmine unit test results in test failure

A StackBlitz has been set up with the karma/jasmine loader for you to view test pass/fail results. The application is functioning correctly. All my tests are expected to pass without any issues, but I am encountering an unusual error when using a mockser ...

Can multiple parameters be passed in a routing URL within Angular 11?

app-routing.module.ts { path: 'projectmodel/:projectmodelname/salespack', component: componentname} When navigating using a button click, I want the URL to be structured in the following way: I attempted to achieve this by using the following co ...

Is there a way for Ionic to remember the last page for a few seconds before session expiry?

I have set the token for my application to expire after 30 minutes, and I have configured the 401/403 error handling as follows: // Handling 401 or 403 error async unauthorisedError() { const alert = await this.alertController.create({ header: 'Ses ...

What causes the error message "Expected ':' when utilizing null conditional in TypeScript?"

UPDATE: After receiving CodeCaster's comment, I realized the issue was due to me using TypeScript version 3.5 instead of 3.7+. It was surprising because these checks seemed to be working fine with other Angular elements, such as <div *ngIf="pa ...

Converting JSON response from REST into a string format in Angular

I have developed a REST service in Angular that sends back JSON response. To perform pattern matching and value replacement, I require the response as a string. Currently, I am utilizing Angular 7 for this task. Below is an example of my service: getUIDa ...

Can you explain the concept of "Import trace for requested module" and provide instructions on how to resolve any issues that

Hello everyone, I am new to this site so please forgive me if my question is not perfect. My app was working fine without any issues until today when I tried to run npm run dev. I didn't make any changes, just ran the command and received a strange er ...

Encountered 'DatePickerProps<unknown>' error while attempting to develop a custom component using Material-UI and react-hook-form

Currently, I'm attempting to create a reusable component using MUI Datepicker and React Hook Form However, the parent component is throwing an error Type '{ control: Control<FieldValues, object>; name: string; }' is missing the follow ...

What is the best approach to create a regex pattern that will identify variables enclosed in brackets across single and multiple lines?

In my Typescript project, I am working on matching all environment variables that are de-structured from process.env. This includes de-structuring on both single and multiple lines. Consider the following examples in TS code that involve de-structuring fr ...