Using TypeScript generics to add constraints to function parameters within an object

My Goal:

Imagine a configuration with types structured like this:

type ExmapleConfig = {
    A: { Component: (props: { type: "a"; a: number; b: number }) => null };
    B: { Component: (props: { type: "b"; a: string; c: number }) => null };
    C: { Component: () => null };
};

Essentially, something like this:

type AdditionalConfigProps = {
    additionalConfigProp?: string;
    // + more additional props that are not optional
};

type ReservedComponentProps = {
    reservedComponentProp: string;
};

type ComponentProps = ReservedComponentProps & Record<string, any>;

type Config = {
    [key: string]: {
        Component: (props: PropsShape) => JSX.Element;
    } & AdditionalConfigProps;
};

I aim to transform a configuration like this, ensuring:

  • The hard types for keys are preserved ('A' | 'B' | 'C' instead of string)
  • The hard types for props are preserved (
    { type: "a"; a: number; b: number }
    instead of Record<string, any>)
  • The transform function only accepts valid configurations:
    • It must have a Component property, and all other properties from AdditionalConfigProps must have correct types
    • It will not accept any additional properties beyond the defined Component and those in AdditionalConfigProps
    • The Component function must be able to accept an object similar to ComponentProps as its first argument

The transformation process may look like this:


const config = {
    A: { Component: (props: { type: "a"; a: number; b: number }) => <div>abc</div> };
    B: { Component: (props: { type: "b"; a: string; c: number }) => <div>abc</div>  };
    C: { Component: () => <div>abc</div>  };
};

/*
    It will extract Components and wrap them with an additional function 
    to return void instead of JSX
*/
const transformedConfig = transformConfig(config);

// typeof transformedConfig
type ResultType = {
    A: (props: { type: "a"; a: number; b: number }) => void;
    B: (props: { type: "b"; a: string; c: number }) => void;
    C: () => void;
};

Note that:

  • Hard types for keys 'A' | 'B' | 'C' were preserved
  • Hard types for 'props' were preserved

My Attempted Approach:

import React from "react";

type AdditionalConfigProps = {
    additionalConfigProp?: string;
};

type ReservedComponentProps = {
    reservedComponentProp: string;
};

const CORRECT_CONFIG = {
    A: {
        Component: (props: { type: "a"; a: number; b: number }) => null,
        additionalConfigProp: "abc"
    },
    B: { Component: (props: { type: "b"; a: string; c: number }) => null },
    C: { Component: (props: { reservedComponentProp: "c"; a: string }) => null },
    D: { Component: (props: {}) => null },
    E: { Component: () => null }
};

const BAD_CONFIG = {
    // Missing Component or other required config prop
    A: {},
    // Incorrect additionalConfigProp
    B: { Component: () => null, additionalConfigProp: 123 },
    // Invalid Component
    C: { Component: 123 },
    // Incorrect component props type
    D: { Component: (props: boolean) => null },
    // Unexpected 'unknownProp'
    E: { Component: () => null, unknownProp: 123 },
    // Invalid 'reservedProp'
    F: { Component: (props: { reservedProp: number }) => null }
};

function configParser<
    Keys extends string,
    ComponentPropsMap extends {
        [Key in Keys]: ReservedComponentProps & Record<string, any>;
    }
>(config: {
    [Key in Keys]: {
        Component: (props?: ComponentPropsMap[Keys]) => React.ReactNode;
    } & AdditionalConfigProps;
}) {
    /*
        TODO: Transform config.
        For now, we want to ensure that TS can handle it correctly.
    */
    return config;
}

/*
    ❌ Throws unexpected type error
*/
const result = configParser(CORRECT_CONFIG);

// Expected typeof result (what I'd want)
type ExpectedResultType = {
    A: {
        Component: (props: { type: "a"; a: number; b: number }) => null;
        additionalConfigProp: "abc";
    };
    B: { Component: (props: { type: "b"; a: string; c: number }) => null };
    C: { Component: (props: { reservedComponentProp: "c"; a: string }) => null };
    D: { Component: (props: {}) => null };
    E: { Component: () => null };
};

/*
    ❌ Should throw type errors, but not the ones it does
*/
configParser(BAD_CONFIG);

One possible solution could be:

function configParser<
    Config extends {
        [key: string]: {
            Component: (componentProps: any) => React.ReactNode;
        };
    }
>(config: Config) {
    return config;
}

// No type error, result type as expected
const result = configParser(CORRECT_CONFIG);

However, this approach:

  • Does not validate componentProps (maybe
    componentProps: Record<string, any> & ReservedComponentProps
    would, but for some reason, it doesn't accept CORRECT_CONFIG)
  • Allows any additional config properties

Answer №1

Consider this potential method:

type ValidateConfiguration<T extends ExtraConfigProperties &
{ Component: (props: any) => void }> =
  { [K in Exclude<keyof T, "Component" | keyof ExtraConfigProperties>]: never } &
  {
    Component: (
      props: Parameters<T["Component"]>[0] extends ComponentProperties ? any : ComponentProperties
    ) => void
  }

declare function generateAppConfig<
  T extends Record<keyof T, ExtraConfigProperties & { Component: (props: any) => void }>>(
    config: T & { [K in keyof T]: ValidateConfiguration<T[K]> }
  ): { [K in keyof T]: (...args: Parameters<T[K]["Component"]>) => void }

The primary goals are:

  • to allow the method `generateAppConfig()` to accept a generic type `T` for the `config` parameter;
  • to restrict `T` to a type that is easier to define, such as `ExtraConfigProperties & {Component: (props: any) => void}>`;
  • to thoroughly validate each property of the inferred `T` by mapping it to a related type `ValidateConfiguration<T[K]>`, ensuring that `T[K] extends ValidateConfiguration<T[K]>` for proper inputs;
  • to compute the return type based on `T`, transforming each property of `T` into a function type determined by indexing into the corresponding `Component` property.

The `ValidateConfiguration<T>` type verifies two aspects:

  • that `T` does not contain any properties not explicitly defined in `ExtraConfigProperties` (or `"Component"`, naturally) by mapping any extra properties to have a `never` type, which will likely fail type checking;
  • that the first parameter of `T`'s `Component` method is assignable to `ComponentProperties` by mapping to `any` if so (a success) and `ComponentProperties` if not (likely a failure due to function type contravariance).

Testing this functionality:

const configuration = {
  A: { Component: (props: { type: "a"; a: number; b: number }) => <div>abc</div> },
  B: { Component: (props: { type: "b"; a: string; c: number }) => <div>abc</div> },
  C: { Component: () => <div>abc</div> }
};
// typeof transformedConfiguration
type ResultType = {
  A: (props: { type: "a"; a: number; b: number }) => void;
  B: (props: { type: "b"; a: string; c: number }) => void;
  C: () => void;
};////
const transformedConfiguration: ResultType = generateAppConfig(configuration);

Validation successful! The compiler accepts `CORRECT_CONFIG` and rejects `BAD_CONFIG` as expected:

const valid = generateAppConfig(CORRECT_CONFIG); // valid
const invalid = generateAppConfig(BAD_CONFIG); // error

Desired functionality achieved.

Playground link to code

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

Styling for older versions of Internet Explorer (IE10 and earlier)

Could it be true that IE 10, 9, and others no longer support conditional statements? Is it also accurate to say that JQuery does not support the browser object above version 1.9? I am facing an issue with CSS rendering differently in Chrome and IE. A Goog ...

The attribute 'commentText' is not found within the 'Comment' data type

Currently, I am immersed in building a user-friendly social network application using Angular 12 for my personal educational journey. Running into an error has left me puzzled and looking for assistance. About the Application: The home page (home.compone ...

Instructions for converting a readonly text field into an editable one using JavaScript

I'm trying to make it so that when the button (with id name_button) is clicked, the text field (with id name_text_field) becomes enabled. However, my current code doesn't seem to be working. Here's a snippet of my HTML: <input type="tex ...

Changing the value within a deeply nested object

I am facing an issue with a nested object in my code: { id: "id", name: "Name", type: "SC", allgemein: { charname: "Name", spieler: "Jon", }, eigenschaften: { lebenspunkte: "30", }, talente: {}, zauber ...

Is there a different term I can use instead of 'any' when specifying an object type in Typescript?

class ResistorColor { private colors: string[] public colorValues: {grey: number, white: number} = { grey: 8, white: 9 } } We can replace 'any' with a specific type to ensure proper typing in Typescript. How do we assign correct ...

Creating a React component with multiple prop types using Typescript

In this particular component, the requirement is to input a config object that can be of two types - either an object containing a "name" property (which should be a string), or a boolean value indicating that the config object has not been set yet. type C ...

Do developers typically define all flux action types within a constants object as a common programming practice?

This question arises from an informative article on flux. The common approach involves defining all action types within a constants object and consistently referencing this object throughout the application. Why is it considered a common practice? What ...

dotdotdot.js is only functional once the window has been resized

For my website, I am attempting to add an ellipsis to multiline paragraphs that exceed a certain height. I have incorporated the dotdotdot jquery plugin from here. An odd issue arises when the page is refreshed, as the ellipsis does not appear until I res ...

Is there a way to make the submit button navigate to the next tab, updating both the URL and the tab's content as well?

I am encountering an issue with my tabs for Step1 and Step2. After pressing the submit button in Step1, the URL updates but the component remains on tab1. How can I resolve this so that the user is directed to the Step2 tab once they click the submit butto ...

Clicking anywhere outside a popup menu in JavaScript will deactivate a toggle and hide it

I have three different menu options: home,Clinic, and About. When I click on the Clinic option, a Megamenu appears, and if I click on it again, the Megamenu is hidden. I want the Megamenu to hide when clicking anywhere on the webpage. The issue is that cu ...

How to exclude the port number from the href in a Node.js EJS template?

I have the following code snippet. I am trying to list out the file names in a specific directory and add an href tag to them. The code seems to be working fine, however, it still includes the port number where my node.js app is running. How can I remove ...

The types for Cypress are not being detected by my Angular tsconfig file

I'm facing an issue with my Angular tsconfig not detecting the Cypress 12.3 types. I have tried numerous solutions to resolve this problem, but nothing seems to work, except for the extreme measure of starting the project over, which I think might sol ...

Exploring the Contrast between Using @import for Styles and js Import for Webpack Configuration

While delving into the source code of ant-design, I couldn't help but notice that each component has both an index.ts and a index.less file in the style folder. This got me thinking - why is JavaScript being used to manage dependencies here? What woul ...

Leveraging React's state to enable temporary invalid numeric input handling

My current approach may be flawed, but I aim to have a parent component and a child component, where the child contains an input field for users to enter numbers. The callback function of the parent component will only be triggered for valid numbers, as ve ...

Is there a way to utilize Typescript enum types for conditional type checking?

I'm working with restful services that accept enum values as either numbers or strings, but always return the values as numbers. Is there a way to handle this in TypeScript? Here's my attempt at it, although it's not syntactically correct: ...

Tips for passing a query parameter in a POST request using React.js

I am new to working with ReactJS and I have a question about passing boolean values in the URL as query parameters. Specifically, how can I include a boolean value like in a POST API call? The endpoint for the post call is API_SAMPLE: "/sample", Here is ...

What steps can I take to minimize the excessive white space below the footer on my website?

I have been attempting to resolve this issue by using multiple methods, but none of them have successfully fixed it. overflow-x: hidden; min-height: 100%; margin: 0px; https://i.sstatic.net/2ctwo.jpg Code: body: background-color: White; margin: 0px; p ...

Typescript does not directly manipulate values. For instance, using a statement like `if(1==2)` is prohibited

I am currently developing an Angular application with a code coverage report feature. There is a method where I need to skip certain lines based on a false condition. So, I attempted to create a function in my component like this: sum(num1:number,num2:nu ...

Eliminate an array from another array if a specific value is present in an object

I've been struggling with removing an entire array if it contains an object with a certain value within. I've searched high and low, but haven't been able to find a solution that fits my specific problem. In my data structure, I have arrays ...

Challenges with handling JSON data in JavaScript

I am currently attempting to retrieve and parse JSON data in order to display it on a blank HTML file. Unfortunately, I keep encountering an issue where if I retrieve and parse the data, I receive an error stating Uncaught TypeError: Cannot read property & ...