Determine the output of a function based on the structure of the input parameter by mapping through a complex nested object

Trying to implement some intricate typing for a project I'm developing, and wondering if it's achievable with TypesScript.

The project in question is a form generator based on schemas and promises, using Vue and TS. It handles UI rendering, validation, data collection from users, all in a functional manner.

Let me illustrate with a simple example:


const { formData, isCompleted } = await formApi.createForm({
  fields: [
    {
        "key": "email",
        "label": "Email address",
        "type": "text",
        "required": true,
        "validators": { "email": isEmail, },
        "size": 8
    },
    {
        "key": "password",
        "label": "Password",
        "type": "password",
        "required": true
    },
  ]
})

In this case, the formData will look like:

{ email: "...", password: "..." }

What I aim to do is type FormData as

{ email: string, password: string }
instead of the current { [key: string]: any } type that's being returned.

This example may be basic, but the library can handle highly complex forms, including nested objects/arrays, conditional rendering, and extensive customization.

UPDATE

After reviewing feedback, I realize that the TypeScript implementation varies greatly based on schema complexity.

Here's an advanced example showcasing the library's capabilities (Live stackblitz example : https://stackblitz.com/edit/vue-xyzwq9?file=src/components/SimpleDependency.vue):

Answer №1

Here is my proposed solution, although @TobiasS's solution also works effectively. It may not be the most visually appealing, but it was intentionally designed to be more "extensible".

type InputTypeMap = {
    text: string;
    textarea: string;
    daterange: [number, number];
    checkbox: boolean;
};

type AddUndefinedIfDependent<T, F extends FormField> = "dependencies" extends keyof F ? T | undefined : T;

type DataFrom<F extends FormField[]> = {
    [K in F[number]["key"]]:
        Extract<F[number], { key: K }> extends infer Field extends FormField
            ? Field["type"] extends keyof InputTypeMap
                ? AddUndefinedIfDependent<InputTypeMap[Field["type"]], Field>
                : Field["type"] extends "object"
                    ? "fields" extends keyof Field
                        ? Field["fields"] extends FormField[]
                            ? AddUndefinedIfDependent<DataFrom<Field["fields"]>, Field>
                            : never
                        : never
                    : Field["type"] extends "array"
                        ? "fields" extends keyof Field
                            ? Field["fields"] extends FormField[]
                                ? AddUndefinedIfDependent<DataFrom<Field["fields"]>[], Field>
                                : never
                            : never
                        : Field["type"] extends "select"
                            ? "options" extends keyof Field
                                ? Field["options"] extends { value: any }[]
                                    ? "fieldParams" extends keyof Field
                                        ? Field["fieldParams"] extends { multiple: true }
                                            ? AddUndefinedIfDependent<Field["options"][number]["value"][], Field>
                                            : AddUndefinedIfDependent<Field["options"][number]["value"], Field>
                                        : never
                                    : never
                                : never
                            : never
            : never;
} extends infer O ? { [K in keyof O]: O[K] } : never;

The essence of this logic is to determine the correct type output. What sets it apart is the utilization of another type for mapping input types to the output type. For instance, daterange translates to [number, number] in InputTypeMap. This feature facilitates the addition of more input types if required. Note that types influenced by other elements in the field should be included in the DataFrom type.

If you are curious about this particular line:

} extends infer O ? { [K in keyof O]: O[K] } : never;

This line simplifies the output type to avoid displaying DataFrom<...> in tooltips.

Below is an example of how you can define your function:

function createForm<I extends FormInit>(
    init: Narrow<I>
): Promise<{
    isCompleted: false;
    formData: undefined;
} | { // using discriminated union so that when isCompleted is false, formData is undefined
    isCompleted: true;
    formData: DataFrom<I["fields"]>;
}> {

Playground

Answer №2

Here is a possible solution to illustrate the concept:

async function generateForm<T extends string>(input: {
  fields: { 
    key: T,
    label: string,
    type: string,
    required: boolean,
    size?: number
  }[]
}): Promise<{ formData: { [K in T]: string }, status: any }> {
  return {} as any
}

In this scenario, the 'key' field contents are stored within the generic type 'T', which can be utilized later to construct a mapped type for the result.

If the structure becomes more intricate, a revamp with additional/varied generic types may be necessary.


Run code sample here


UPDATE:

Below is a potential solution tailored to your revised specifications.

type FormDataStructure<N> = {
  key: N
  label: string
  type: N
  mandatory?: boolean
  size?: number
  details?: {
    multipleValues: boolean,
    editable: boolean,
  },
  conditions?: (...args: any) => any
  subfields?: FormDataStructure<N>[]
}

The definition of 'FormDataStructure' outlines the setup of the argument for 'generateForm'. Properties not pertinent to the query have been omitted.

type DetermineFormDataType<K extends FormDataStructure<any>> =  
  K["type"] extends "checkbox"
    ? boolean
    : K["type"] extends "object"
      ? K["subfields"] extends infer U extends FormDataStructure<any>[]
        ? DataReturnType<U[number]>
        : never
      : K["type"] extends "select"
        ? K["details"] extends { multipleValues: true }
          ? string[]
          : string
        : K["type"] extends "array"
          ? K["subfields"] extends infer U extends FormDataStructure<any>[]
            ? DataReturnType<U[number]>[]
            : never
          : K["type"] extends "daterange"
            ? [string, string]
            : string

'DetermineFormDataType' examines an object from the 'subfields' and determines its type based on the 'type' property.

type DataReturnType<T extends FormDataStructure<any>> = UnionToIntersection<{
  [K in T as K["conditions"] extends (...args: any) => any ? never : K["key"]]: 
    DetermineFormDataType<K>
} | {
  [K in T as K["conditions"] extends (...args: any) => any ? K["key"] : never]?: 
    DetermineFormDataType<K>
}>

'DataReturnType' recursively traverses the 'subfields' and decides which properties should be optional.

type UnionToIntersection<U> = 
  (U extends any ? (k: U)=>void : never) extends ((k: infer I)=>void) ? I : never

type RecursiveExpansion<T> = T extends object
  ? T extends infer O ? { [K in keyof O]: RecursiveExpansion<O[K]> } : never
  : T;
type Narrowable = string | number | boolean | symbol | object | undefined | void | null | {};

async function generateForm<T extends FormDataStructure<N>, N extends Narrowable>(input: {
  fields: T[]
}): Promise<{ status: any, formData: RecursiveExpansion<DataReturnType<T>> } > {
  return {} as any
}

Test playground scenario here

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

Adding the expiry date/time to the verification email sent by AWS Cognito

After some investigation, I discovered that when a user creates an account on my website using AWS Cognito, the verification code remains valid for 24 hours. Utilizing the AWS CDK to deploy my stacks in the AWS environment, I encountered a challenge within ...

MQTT: The method net.createConnection does not exist

Following the guide at https://www.npmjs.com/package/mqtt#install to establish an mqtt connection, I encountered a render error indicating _$$_REQUIRE_(dependencyMap[1], "net").createConnection(port, host)','_$$_REQUIRE(_dependencyMap[ ...

Collaborate and apply coding principles across both Android and web platforms

Currently, I am developing a web version for my Android app. Within the app, there are numerous utility files such as a class that formats strings in a specific manner. I am wondering if there is a way to write this functionality once and use it on both ...

Issue: Angular ERROR TypeError - Cannot access the property 'push' of a null value

In my code, I have a property called category = <CategoryModel>{};. The CategoryModel model looks like this: export class CategoryModel { public name: string; public description: string; public image: string; public products?: ProductModel[]; ...

A guide on retrieving the values of all child elements within an HTML element using Puppeteer

I have been exploring the capabilities of puppeteer and am trying to extract the values from the column names of a table. <tbody> <tr class="GridHeader" align="center" style="background-color:Black;"> <td cl ...

Angular mat-select is having difficulty displaying options correctly on mobile devices or devices with narrow widths

In my Angular project, I've encountered an issue with mat-select when viewing options on mobile or low-resolution screens. While the options are still displayed, the text is mysteriously missing. I attempted to set the max width of the mat-option, but ...

Transforming JSON keys in Angular

As a newcomer to angular and API integration, I am facing an issue with ngCharts in my project. The chart specifically requires the keys names in JSON to be "value" and "name", but the API I am using provides keys named "count" and "label". Is there a way ...

Differences between useFormState and useForm in Next.js version 14

Currently, I am intrigued by the comparison between using the react hook useForm and the react-dom useFormState. The Nextjs documentation suggests utilizing useFormState, but in practice, many developers opt for the react hook useForm. I am grappling with ...

"Continue to shine until the rendering function displays its source code

I've encountered a strange issue where I'm using lit until to wait for a promise to return the template, but instead of the desired output, the until's function code is being rendered. For example: render() { return html` <div c ...

Can you tell me the data type of IconButtons in Material UI when using TypeScript?

Currently, I am in the process of creating a sidebar using Material UI in Next JS with typescript. My plan is to create a helper function that will help display items within the sidebar. // LeftSidebar.tsx import {List,ListItem,ListItemButton,ListItemIcon ...

When utilizing the Map.get() method in typescript, it may return undefined, which I am effectively managing in my code

I'm attempting to create a mapping of repeated letters using a hashmap and then find the first non-repeated character in a string. Below is the function I've developed for this task: export const firstNonRepeatedFinder = (aString: string): strin ...

Using the spread operator to modify an array containing objects

I am facing a challenge with updating specific properties of an object within an array. I have an array of objects and I need to update only certain properties of a single object in that array. Here is the code snippet I tried: setRequiredFields(prevRequir ...

Unable to directly assign a variable within the subscribe() function

My goal is to fetch a single row from the database and display its information on my webpage. However, I've encountered an issue with the asynchronous nature of subscription, which prevents immediate execution and access to the data. Upon running the ...

Subscribing with multiple parameters in RxJS

I am facing a dilemma with two observables that I need to combine and use in subscribe, where I want the flexibility to either use both arguments or only one. I have experimented with .ForkJoin, .merge, .concat but haven't been able to achieve the des ...

Finding the file path to a module in a NextJS application has proven to be a challenge when utilizing the module

Currently, I am utilizing the webpack plugin module-federation/nextjs-mf, which enables us to work with a micro-frontend architecture. Based on the official documentation and referencing this particular example, it is possible to share components between ...

Verifying the accuracy of a React Component in interpreting and displaying a path parameter

I have the following React/Typescript component that is functioning correctly. However, I am struggling to write a test using testing-library. My goal is to verify that it properly receives the level parameter and displays it on the page: import React from ...

Attempting to use a string as an index for the type `{ firstName: { inputWarning: string; inputRegex: RegExp; }` is not allowed

const checkRegexSignUp = { firstName: { inputWarning: "only letters", inputRegex: /^[a-z ,.'-]+$/i }, lastName: { inputWarning: "only letters", inputRegex: /^[a-z ,.'-]+$/i }, } const change = (e: ChangeEvent<HT ...

developed a website utilizing ASP MVC in combination with Angular 2 framework

When it comes to developing the front end, I prefer using Angular 2. For the back end, I stick with Asp MVC (not ASP CORE)... In a typical Asp MVC application, these are the steps usually taken to publish the app: Begin by right-clicking on the project ...

What could be causing the lack of updates for my service on the app component?

I am currently using Ionic 4 and facing an issue with displaying information about the logged-in user. The code works perfectly in all components except for the app component. I have a variable named userData which is a BehaviorSubject. Can someone help me ...

The system detected an Image with the source "/images/logo.png" as the primary element contributing to Largest Contentful Paint (LCP)

I have been working on a project using Next.13 and Typescript. In order to display an Image, I created a component called Logo.tsx. "use client"; import Image from "next/image"; import { useRouter } from "next/navigation"; c ...