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

Utilizing Typescript for manipulation of Javascript objects

Currently, I am working on a project using Node.js. Within one of my JavaScript files, I have the following object: function Person { this.name = 'Peter', this.lastname = 'Cesar', this.age = 23 } I am trying to create an instanc ...

Number as the Key in Typescript Record<number, string> is allowed

As a newcomer to TypeScript, I am still learning a lot and came across this code snippet that involves the Record utility types, which is quite perplexing for me. The code functions correctly in the playground environment. const data = async (name: string ...

Show categories that consist solely of images

I created a photo gallery with different categories. My goal is to only show the categories that have photos in them. Within my three categories - "new", "old", and "try" - only new and old actually contain images. The issue I'm facing is that all t ...

Utilizing the variables defined in the create function within the update function of Phaser 3

I'm facing an issue in my game where I can't access a variable that I declared in the create function when trying to use it in the update function. Here is a snippet of what I'm trying to achieve: create() { const map = this.make. ...

Integrate a fresh global JSX attribute into your React project without the need for @types in node_modules

It is crucial not to mention anything in tsconfig.json. Error Type '{ test: string; }' cannot be assigned to type 'DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>'. Property 'test' does not exi ...

Having trouble showing the fa-folders icon in Vuetify?

Utilizing both Vuetify and font-awesome icons has been a successful combination for my project. However, I am facing an issue where the 'fa-folders' icon is not displaying as expected: In the .ts file: import { library } from '@fortawesome/ ...

The TypeScript error message indicates that the property 'forEach' is not found on the 'FileList' type

Users are able to upload multiple files on my platform. After uploading, I need to go through each of these files and execute certain actions. I recently attempted to enhance the functionality of FileList, but TypeScript was not recognizing the forEach m ...

The confusion arises from the ambiguity between the name of the module and the name of

In my current scenario, I am faced with the following issue : module SomeName { class SomeName { } var namespace = SomeName; } The problem is that when referencing SomeName, it is pointing to the class instead of the module. I have a requireme ...

Executing a NestJs cron job at precise intervals three times each day: a guide

I am developing a notifications trigger method that needs to run three times per day at specific times. Although I have reviewed the documentation, I am struggling to understand the regex code and how to customize it according to my requirements! Current ...

Encountering a Nuxt error where properties of null are being attempted to be read, specifically the 'addEventListener' property. As a result, both the default

Currently, I am utilizing nuxt.js along with vuesax as my UI framework. I made particular adjustments to my default.vue file located in the /layouts directory by incorporating a basic navbar template example from vuesax. Subsequently, I employed @nuxtjs/ ...

Determine the generic types of callback in TypeScript based on the argument provided

There are numerous Stack Overflow questions that share a similar title, but it seems none of them address this particular inquiry. I'm in the process of developing a wrapper for an express RequestHandler that can catch errors in asynchronous handlers ...

Show blank value if there are no search results, along with an additional menu option

I am currently working on a Typeahead feature with a customized menu using the renderMenu method. In this setup, I have added 2 custom menu items at the bottom - one divider and a button. An issue arises when there are no search results. If I do not inclu ...

Eliminate nested object properties using an attribute in JavaScript

I am working with a nested object structured like this const data = [ { id: '1', description: 'desc 1', data : [ { id: '5', description: 'desc', number :1 }, { id: '4', description: 'descip& ...

Exploring Sequelize: Uncovering the Secret to Retrieving Multiple Associated Items of Identical Type

Within my database, I have a situation where there are two tables sharing relations of the same type. These tables are named UserCollection and ImagenProcess UserCollection has two instances that relate to ImagenProcess. Although the IDs appear unique whe ...

The ngOnChanges lifecycle hook is triggered only once upon initial rendering

While working with @Input() data coming from the parent component, I am utilizing ngOnChanges to detect any changes. However, it seems that the method only triggers once. Even though the current value is updated, the previous value remains undefined. Below ...

Updating state atoms in Recoil.js externally from components: A comprehensive guide for React users

Being new to Recoil.js, I have set up an atom and selector for the signed-in user in my app: const signedInUserAtom = atom<SignedInUser | null>({ key: 'signedInUserAtom', default: null }) export const signedInUserSelector = selecto ...

Is there a way to incorporate several select choices using specific HTML?

I am currently trying to dynamically populate a select tag with multiple option tags based on custom HTML content. While I understand how to insert dynamic content with ng-content, my challenge lies in separating the dynamic content and wrapping it in mat ...

What are the TypeScript type definitions for the "package.json" configuration file?

What is the most efficient method for typing the content of the "package.json" file in TypeScript? import { promises as fs } from 'fs'; export function loadManifest(): Promise<any> { const manifestPath = `${PROJECT_DIR}/package.json`; ...

Trouble retrieving query parameters from a URL while trying to access URL parameters from a module

I am currently learning angular and facing a small problem that I'm unsure how to solve. My module looks like this: const hostHandler = setContext((operation: any, context: any) => ({ headers: { ...context?.headers, 'X-Location-Hostn ...

The never-ending scroll feature in Vue.js

For the component of cards in my project, I am trying to implement infinite scrolling with 3 cards per row. Upon reaching the end of the page, I intend to make an API call for the next page and display the subsequent set of cards. Below is my implementatio ...