Utilizing TypeScript to define the parameter of a method within a generic interface by extracting a value from the generic type

In search of defining a versatile interface that can manage any Data type, I came up with an idea. This interface includes a dataKey property which simply holds a value of keyof Data. Additionally, it features a handler function where the parameter type should align with the type retrieved when using dataKey to access a value from Data. The concept is outlined below, although it fails as Data[dataKey] isn't valid in TypeScript:

interface Handler<Data> {
    dataKey: keyof Data,
    handler: (value: Data[dataKey]) => void
}

Is there a method to make this approach operational? Substituting Data[dataKey] with the any type might seem like a quick fix, but it sacrifices type safety.

Below exemplifies how I envision utilizing the Handler interface:

function handleData<Data extends object>(data: Data, handler: Handler<Data>) {
    const value = data[handler.dataKey];
    handler.handler(value);
}

interface Person {
    name: string,
    age: number,
}

const person: Person = {name: "Seppo", age: 56};
const handler: Handler<Person> = {dataKey: "name", handler: (value: string) => {
    // Here we are certain about the type of `value` being string,
    // derived from accessing `name` within the person object.
    // Changing the dataKey to "age" should result in
    // the type of `value` being `number`, respectively
    console.log("Name:", value);
}}

handleData(person, handler);

Answer №1

When it comes to making Handler<Person> a type that ensures the connection between the dataKey property and the handler callback parameter type, utilizing a union type with one member for each of Person's properties is essential. The structure would have to resemble this:

interface Person {
  name: string,
  age: number,
}

type PersonHandler = Handler<Person>;
/* type PersonHandler = {
    dataKey: "name";
    handler: (value: string) => void;
} | {
    dataKey: "age";
    handler: (value: number) => void;
} */

In this way, a Handler<Person> can be either of type

{dataKey: "name", handler: (value: string) => void}
, or of type
{dataKey: "age", handler: (value: number) => void}
.

The aim is to define

type Handler<T extends object> = ...
in a manner that constructs the appropriate union type. Since interfaces cannot represent union types, I've altered Handler from an interface to a type alias.


Here's one approach to achieve this:

type Handler<T> = { [K in keyof T]-?: {
  dataKey: K,
  handler: (value: T[K]) => void
} }[keyof T]

This is referred to as a distributive object type introduced in microsoft/TypeScript#47109. A distributive object type is essentially a mapped type immediately subjected to indexing to yield a union of its property types. If you have a union of keys denoted by type K = K1 | K2 | K3 | ... | KN, then the distributive object type {[P in K]: F<P>}[K] translates to

F<K1> | F<K2> | F<K3> | ... | F<KN>
.

In the described Handler<T> definition, we loop over the keys of T while eliminating any optional properties using the -? mapping modifier to prevent unwanted undefined types in the output. For every key K, we generate the corresponding dataKey/handler pair. Subsequently, we index into it using the same set of keys to obtain the union of all such values.

With this definition in place, Handler<Person> materializes as the intended type:

type PersonHandler = Handler<Person>;
/* type PersonHandler = {
    dataKey: "name";
    handler: (value: string) => void;
} | {
    dataKey: "age";
    handler: (value: number) => void;
} */

This also aligns with the desired outcome when assigning like so:

const handler: Handler<Person> = {
  dataKey: "name", handler: value => {
    console.log("Name:", value.toUpperCase());
  }
}

It's important to note that specifying that value is a string is unnecessary; the compiler infers contextually that value must be a

string</code since <code>Handler<Person>
constitutes a discriminated union, with the discriminant being "name".

Link to playground with the 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

Discovering a solution to extract a value from an Array of objects without explicitly referencing the key has proven to be quite challenging, as my extensive online research has failed to yield any similar or closely related problems

So I had this specific constant value const uniqueObjArr = [ { asdfgfjhjkl:"example 123" }, { qwertyuiop:"example 456" }, { zxcvbnmqwerty:"example 678" }, ] I aim to retrieve the ...

Awaiting the completion of Promises within a for-loop (Typescript)

I'm struggling with a for-loop and promises in my angular2 project. I have multiple methods that return promises, and after these promises are resolved, I want to populate an array in the class using Promise.all(variable).then(function(result){....... ...

What is the best way to save code snippets in Strapi for easy integration with SSG NextJS?

While I realize this may not be the typical scenario, please listen to my situation: I am using Strapi and creating components and collections. One of these collections needs to include code snippets (specifically typescript) that I have stored in a GitH ...

The Vue Typescript callback is automatically assigned the type "any" when not explicitly defined

Encountering a TypeScript compiler error while using an anonymous function with lodash debounce in my Vue component's watch option. The error states: "this implicitly has type any." Below is the code snippet of my component: export default defineComp ...

Angular 13's APP_INITIALIZER doesn't wait as expected

Recently, I have been in the process of upgrading from okta/okta-angular version 3.x to 5.x and encountered an unexpected bug. Upon startup of the application, we utilized APP_INITIALIZER to trigger appInitializerFactory(configService: ConfigService), whi ...

Converting a text file to JSON in TypeScript

I am currently working with a file that looks like this: id,code,name 1,PRT,Print 2,RFSH,Refresh 3,DEL,Delete My task is to reformat the file as shown below: [ {"id":1,"code":"PRT","name":"Print"}, {" ...

Determine the field's type without using generic type arguments

In my code, there is a type called Component with a generic parameter named Props, which must adhere to the Record<string, any> structure. I am looking to create a type that can accept a component in one property and also include a function that retu ...

Having trouble retrieving the value of an HTML input field using form.value in Angular 5?

I am currently working with Angular 5 Within my HTML, I am dynamically populating the value of an input field using: <input type="number" class="form-control" id="unitCost" name="unitCost" [(ngModel)]="unitCost" placeholder="Average Unit Price"> ...

Puppeteer: What is the best way to interact with a button that has a specific label?

When trying to click on a button with a specific label, I use the following code: const button = await this.page.$$eval('button', (elms: Element[], label: string) => { const el: Element = elms.find((el: Element) => el.textContent === l ...

Encountering issues with dependencies while updating React results in deployment failure for the React app

Ever since upgrading React to version 18, I've been encountering deployment issues. Despite following the documentation and scouring forums for solutions, I keep running into roadblocks with no success. The errors displayed are as follows: $ npm i np ...

What is the best approach for managing Create/Edit pages in Next.js - should I fetch the product data in ServerSideProps or directly in the component?

Currently, I am working on a form that allows users to create a product. This form is equipped with react-hook-form to efficiently manage all the inputs. I am considering reusing this form for the Edit page since it shares the same fields, but the data wil ...

What is the TypeScript declaration for the built-in 'net' NodeJS types?

I'm currently working on developing a TCP client application and it seems like the 'net' package in NodeJs will be perfect for what I need. However, I've run into an issue finding the TypeScript definitions for this package. If you hav ...

Transform TypeScript class into an object

Is there a way to transfer all values from one typescript class, Class A, to another matching class, Class B? Could there be a method to extract all properties of Class A as an object? ...

Issue with SvelteKit: PageData not being refreshed in API response after initial render

I am relatively new to working with Svelte and SvelteKit, and I am currently trying to fetch data from an API. I have followed the SvelteKit todo sample code, which works well for the initial rendering and when clicking on an a tag. However, I am facing an ...

Tips for isolating data on the current page:

Currently, I am using the igx-grid component. When retrieving all data in one call and filtering while on the 3rd page, it seems to search through the entire dataset and then automatically goes back to "Page 1". Is there a way to filter data only within th ...

What could be the reason for my dynamic image not appearing in a child component when using server-side rendering in Nuxt and Quasar

Currently, I am tackling SSR projects using Nuxt and Quasar. However, I encountered an issue when trying to display a dynamic image in a child component as the image is not being shown. The snippet of my code in question is as follows: function getUrl (im ...

I'm having trouble importing sqlite3 and knex-js into my Electron React application

Whenever I try to import sqlite3 to test my database connection, I encounter an error. Upon inspecting the development tools, I came across the following error message: Uncaught ReferenceError: require is not defined at Object.path (external "path ...

Limiting the height of a grid item in MaterialUI to be no taller than another grid item

How can I create a grid with 4 items where the fourth item is taller than the others, determining the overall height of the grid? Is it possible to limit the height of the fourth item (h4) to match the height of the first item (h1) so that h4 = Grid height ...

Ways to prevent scrolling in Angular 6 when no content is available

I am developing an angular 6 application where I have scrollable divs containing: HTML: <button class="lefty paddle" id="left-button"> PREVIOUS </button> <div class="container"> <div class="inner" style="background:red">< ...

The circular reference error message "Redux Action 'Type alias 'Action' circularly references itself" appears

I am currently working with two different actions: export const addToCart = (id: string, qty: number) => async ( dispatch: Dispatch, getState: () => RootState ) => { const { data }: { data: IProduct } = await axios.get(`/api/products/${id}`) ...