Determine the class of an object within the "keyof" parameter by utilizing both property and generic types

I have a requirement to create an interface with generic types that can accept an object with keys representing "root field names" and values as arrays of objects defining sub-fields with the key as the name of the sub-field and the type as the value's data type. Here is an example:

interface Inputs{
    emails: { email: string, active: boolean, i: number }[]
}

const obj:Inputs = {emails: [ {email: "example@mail.com", active: true, i: 100} ]}

The interface which receives this as a generic type should have a "name" property that will take the (keyof) name of the sub-field (e.g., active) and a function with a parameter that must receive the type of the sub-field defined in the name property.

Here is an example:

    [
      {
        name: "active",
        component: ({ value, values }) => {
          console.log(value, values);
          return <>Component</>;
        }
      }
    ]

In this scenario, the 'value' variable should only accept the type 'boolean' since the 'active' key in the object has a boolean type.

I have managed to achieve almost everything I intended to do. The only issue is that instead of receiving the exact type of the subfield, the function's parameter gets a union of all types present in the object.

For instance, in the previous example, since 'email' is a string type, 'value' should also be a string type, but it actually becomes string | number | boolean (all available types in the object).

I hope I have explained this clearly enough, but I have set up a sandbox for better clarity

https://codesandbox.io/s/boring-fast-pmmhxx?file=/src/App.tsx

interface Options<
  T extends { [key: string]: unknown }[],
  Key extends keyof T[number]
> {
  values: T;
  value: Key;
}

interface InputDef<
  T extends { [key: string]: any }[],
  Key extends keyof T[number]
> {
  name: Key;
  component: (props: Options<T, T[number][Key]>) => React.ReactNode;
}

interface Props<T extends { [key: string]: [] }, Key extends keyof T> {
  name: Key;
  inputs: InputDef<T[Key], keyof T[Key][number]>[];
  callback: (values: T) => void;
}

interface Inputs {
  firstName: string;
  lastName: string;
  emails: { email: string; active: boolean; other: number }[];
}

const GenComponent = <T extends { [key: string]: any }, Key extends keyof T>({
  name,
  inputs
}: Props<T, Key>) => {
  console.log(inputs);
  return (
    <div>
      {name} {JSON.stringify(inputs)}
    </div>
  );
};

interface MainComponentProps {
  callback: TestCallback<Inputs>;
}

const MainComponent: React.FC<MainComponentProps> = ({ callback }) => {
  return (
    <>
      <GenComponent
        callback={callback}
        name="emails"
        inputs={[
          {
            name: "active",
            component: ({ value, values }) => {
              console.log(value, values);
              return <>Component</>;
            }
          }
        ]}
      />
    </>
  );
};

type TestCallback<Data> = (values: Data) => void;

function test<Data>(values: Data): void {
  console.log(values);
}

export default function App() {
  return (
    <div className="App">
      <MainComponent callback={test} />
    </div>
  );
}

On line 57, given that the name in the object is "active", the type of 'value' should ideally be "boolean" rather than "string | number | boolean". How can I resolve this?

Thank you!

Answer №1

To address the issue at hand, we need to simplify the example and provide a solution. Let's break it down by examining the generic type KeyValFunc<T, K>. This type takes an object type T and one of its key types K, storing both the key and a function that accepts a value matching the property for that key:

interface KeyValFunc<T, K extends keyof T> {
    key: K,
    valFunc: (val: T[K]) => void
}

Now let's consider implementing this interface using the following object type Foo:

interface Foo {
    x: number,
    y: string,
    z: boolean
}

We can create an instance of

KeyValFunc<Foo, "x">
with key as "x" and valFunc as (val: number) => void:

const obj: KeyValFunc<Foo, "x"> = { key: "x", valFunc: val => val.toFixed() }; // works fine

Next, the challenge arises when trying to construct an array of KeyValFunc<T, keyof T>. Although the attempt seems logical, it encounters errors due to not recognizing the data types correctly:

const arr: KeyValFuncArray<Foo> = [
    { key: "x", valFunc: val => val.toFixed() } // error!
]

The root cause lies in how KeyValFunc<T, keyof T> is structured. To better understand this, let's evaluate it specifically for Foo:

type Test = KeyValFunc<Foo, keyof Foo>;
/* type Test = KeyValFunc<Foo, keyof Foo> */;

By introducing an identity mapped type, we gain insights into each property within KeyValFunc:

type Id<T> = { [K in keyof T]: T[K] };

type Test = Id<KeyValFunc<Foo, keyof Foo>>;
/* type Test = {
     key: keyof Foo;
     valFunc: (val: string | number | boolean) => void;
} */

This reveals where the problem stems from - using keyof T indiscriminately introduces unwanted flexibility. Instead, we should devise a method to distribute KeyValFunc<T, K> across unions in K, ultimately aligning with our intended output.

A potential solution involves crafting a new mapped type:

type SomeKeyValueFunc<T> = { [K in keyof T]-?: KeyValFunc<T, K> }[keyof T]

Through this adjustment, we successfully generate the expected union properties. Consequently, for constructing arrays like KeyValFuncArray<T>, utilizing

SomeKeyValueFunc<T></code proves more efficient than incorporating <code>KeyValueFunc<T, keyof T>
:

type KeyValFuncArray<T> = SomeKeyValueFunc<T>[];

When executed in practice, the revised approach operates seamlessly, yielding the desired outcomes consistently.

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

Sometimes the downloaded xlsx file may become corrupted

Currently, I am working on developing a project using Angular4 with Typescript. One of the tasks involved creating a blob utilizing the XLSX-populate library. Below is an example showing the code snippet for generating a valid xlsx object: var url = wind ...

Filtering data in TypeScript from a different component with the filter function

Currently, I am in the process of creating a filter function for a table. The table header and table body are separate components - with one responsible for filtering the data and the other for displaying it. User input is stored in the Table Header Compo ...

The type 'Text' does not have a property named 'then'

Transitioning from .js to typescript. When I changed the file extension from .js to .ts while keeping the same code, I encountered an error stating Property 'then' does not exist on type 'Text'.ts in the then((value) method. The return ...

Troubleshooting: HTTP client post request working with body.set but not with formData.append for sending data to API

I am in the process of updating the UX for an older application with APIs developed in ASP.NET When I make a POST request as shown below, everything works perfectly. The data is received: var APIURL = sessionStorage.getItem('endpoint') + "/ ...

Unable to access attribute of instantiated class

I am relatively new to TypeScript and I recently encountered a problem that's stumping me. I'm working on setting up a REST API using Express. The setup involves a router that calls a controller, which in turn invokes a service method before ret ...

What is the significance of incorporating react context, createContext, useContext, and useStore in Mobx?

In my Typescript application, I rely on Mobx for persistence and have created a singleton class called MyStore to manage the store: export class MyStore { @observable something; @observable somethingElse; } export myStore:MyStore = new MyStore(); ...

I can't decide which one to choose, "ngx-bootstrap" or "@ng-bootstrap/ng-bootstrap."

Currently, I am in the process of deciding whether to use Bootstrap 4 with angular 4 for my upcoming project. However, I find myself torn between choosing npm install --save @ng-bootstrap/ng-bootstrap or npm install ngx-bootstrap --save. Could someone pl ...

Implementing the strictNullCheck flag with msbuild

Can strict null checks be enabled when compiling using msbuild? I see in the documentation that the compiler option is --strictNullChecks, but I couldn't find any specific entry for it on the msbuild config page. Is there a method to activate this f ...

"Typescript: Unraveling the Depths of Nested

Having trouble looping through nested arrays in a function that returns a statement. selectInputFilter(enteredText, filter) { if (this.searchType === 3) { return (enteredText['actors'][0]['surname'].toLocaleLowerCase().ind ...

An endless cascade of dots appears as the list items are being rendered

Struggling to display intricately nested list elements, Take a look at the JSON configuration below: listItems = { "text": "root", "children": [{ "text": "Level 1", "children": [{ "text": "Level 2", "children": [{ "text": ...

Utilizing Angular 7 to extract data from the initial column of an Excel spreadsheet and store it within an array

Currently, I am in the process of uploading an excel file that contains an ID column as its first column. My goal is to extract all the IDs and store them in an array for future data management purposes. To accomplish this task, I am utilizing the XLSX l ...

Issues with mat-tab-group not rendering properly after switching between parent tabs

I am facing an issue involving nested tabs and tables in my example. Check out the example here After switching between parent tabs and child tabs, there seems to be an issue where the tabs do not render properly. It takes multiple attempts of switching ...

Typescript: Utilizing a generic array with varying arguments

Imagine a scenario where a function is called in the following manner: func([ {object: object1, key: someKeyOfObject1}, {object: object2, key: someKeyOfObject2} ]) This function works with an array. The requirement is to ensure that the key field co ...

A simple trick to compile and run TypeScript files with just one command!

Converting TS to JS is typically done using the tsc command, followed by executing the resulting .js file with node. This process involves two steps but is necessary to run a .ts file successfully. I'm curious, though, if there is a way to streamlin ...

Execute different commands based on operating system using NPM conditional script for Windows and Mac

Scenario: I am currently configuring a prepublishOnly hook in NPM. This hook is designed to remove the "lib" folder, transpile the typescript source files into a new lib folder, and then execute the tests. The issue at hand: There are two individuals re ...

Utilizing React Bootstrap with TypeScript for Styling Active NavItem with Inline CSS

Is it possible to change the background color of the active NavItem element to green using inline CSS in React Bootstrap and React Router Dom? I am currently using TypeScript 2.2 and React. If not, should I create a CSS class instead? Here is the code sni ...

Tips for eliminating inline CSS usage in React

Is it possible to avoid using inline CSS in React when styling an element like this? const dimensionStyles = { width: 10, height: 20 }; <div className="point-class" style={{ width: dimensionStyles.width + "px", height: ...

Joining the Parent Route String with a Variable using Angular Routerlink

How can I incorporate string interpolation or concatenation into the router link below in order to navigate to the parent route and include a variable link? <a routerLink="../account-information/{{item.productId}}"> ...

The challenge with the Optional Chaining operator in Typescript 3.7@beta

When attempting to utilize the Typescript optional chaining operator, I encountered the following exception: index.ts:6:1 - error TS2779: The left-hand side of an assignment expression may not be an optional property access. Here is my sample code: const ...

TypeScript's HashSet Implementation

I'm working on a simple TypeScript task where I need to extract unique strings from a map, as discussed in this post. Here's the code snippet I'm using: let myData = new Array<string>(); for (let myObj of this.getAllData()) { let ...