A function in Typescript is created to handle diverse input types in a generic manner

My goal is to create a function that can handle various input types for abstraction purposes.

type ContentA = string

type ContentB = number

type InputA = {
 name: 'method_a'
 content: ContentA
}

type InputB = {
 name: 'method_b'
 content: ContentB
}

type Input = InputA | InputB

Each type of input corresponds to a different method:

const my_methods = {
 method_a: (content:ContentA) => {
  // ...
 },
 method_b: (content:ContentB) => {
  // ...
 }
}

Now I need to create a generic function that can handle all the possible inputs, as there could be many. Currently, there are only two types of inputs defined, but in my actual project, there are around 16.

I attempted to implement this kind of function, but encountered a compilation error:

function foo(input:Input){
 return my_methods[input.name](input.content);
                             // ^
                             // | Argument of type 'string | number' is not  
                             // | assignable to parameter of type 'never'.
                             // | Type 'string' is not assignable to type 'never'.
}

Is there a way for Typescript to understand that the argument passed to the method based on input.name will always match with input.content? How can I resolve this issue?

Playground link

Answer №1

This challenge presented an intriguing problem, and after some thoughtful consideration, I believe I've devised a satisfactory solution.

My approach involved transforming the initial union type Input into generics instead of relying on discriminated unions based on literals, as those are more effective with actual values rather than variables.

To start, let's define a type that encompasses all potential values for the name property:

type Names = Input["name"];

Subsequently, we can establish a "lookup" generic type that correlates the given name type argument with the corresponding content type. For instance, ContentByName<"method_a"> would refer to ContentA.

type ContentByName<TName extends Names> = {
  [i in Input as i["name"]]: i["content"];
}[TName];

With this setup, we create a specific type tailored to your my_methods object, ensuring clarity for the compiler regarding the associations between names and content types:

type Methods = { [name in Names]: (content: ContentByName<name>) => void };

const my_methods: Methods = { // <-- introduced in your code here
  // ...
}

Finally, your foo function must also be made generic, necessitating a generic rendition of the Input type.

type InputByName<TName extends Names> = {
  name: TName;
  content: ContentByName<TName>;
};

function foo<TName extends Names>(input: InputByName<TName>) {  // <-- included
  //...
}

It's worth noting that you can still invoke this function with a regular Input just like before. This remains perfectly valid:

function foo_old(input: Input) {
    return foo(input);
}

No changes were actually made to the types themselves; we simply facilitated the compiler's understanding of them.

For the modified version along with these alterations, visit the playground link demonstrating these changes.

Answer №2

It's important to note that the code provided in the playground differs significantly from what was mentioned in the original question.

The main issue with this snippet is the lack of effective type narrowing. One way to address this would be by utilizing a switch statement:

function bar(input: Input) {
    switch(input.name) {
        case 'method_x': {
            // At this point, input.content is known to be a string
            console.log(input.content)
            break;
        }
        case 'method_y': {
            // Here, input.content is confirmed to be a number
            console.log(input.content)
            break;
        }
    }
}

Answer №3

Here is one potential solution that I can offer:

Utilize the playground link

const isOfTypeInputA = (input:Input): input is InputA => {
    return input.name === 'method_a';
}

const isOfTypeInputB = (input:Input): input is InputB => {
    return input.name === 'method_b';
}

function foo(input:Input){
    if (isOfTypeInputA(input)) {
        return my_methods.method_a(input.content);
    } else if (isOfTypeInputB(input)) {
        return my_methods.method_b(input.content);
    } else {
        throw new Error(`foo Not supported input`);
    }
}

Answer №4

It seems like determining the type of my_methods[input.name] is a challenge as it cannot be strictly defined at compile time beyond

(ContentA) => void | (ContentB) => void
.

To address this, a cast is necessary:

function foo(input:Input){
 return my_methods[input.name](input.content as any);
 //                                          ^^^^^^
}

Some other solutions presented do not require a cast and instead rely on control flow for type narrowing, which are also valid options depending on context and preference.

If you choose to continue with the current method, one enhancement could be to define the type of my_methods more precisely so that the compiler can verify if the keys and argument types align with the potential Input types:

type InputMethods = {
    [I in Input as I['name']]: (content: I['content']) => void
}

const my_methods: InputMethods = {
    method_a: (content: ContentA) => {
        // ...
    },
    method_b: (content: ContentB) => {
        // ...
    }
    
}

Playground

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

Challenges in designing components in Angular 2.0 and beyond

Issue at hand - There are two input controls on the same page, each belonging to separate components. When a value is entered into the first input box, it calculates the square value and updates the second input control accordingly. Conversely, if the v ...

Tips for managing numerous nested subscriptions

Looking to extract the id parameter from the route, fetch the corresponding task, and then its parent if applicable. Angular CLI: 7.1.4 Node: 11.6.0 OS: linux x64 Angular: 7.1.4 @angular-devkit/architect 0.11.4 @angula ...

Issues with Typegoose and Mongoose Enums when utilizing an array of strings

One of my enums is defined as follows: export enum Careers { WEB_DEVELOPMENT = 'Web Development', MOBILE_DEVELOPMENT = 'Mobile Development', UI_UX = 'UI/UX' } This particular enum is used as a mongoose property like so: ...

The functionality to verify the presence of a child element is not functioning correctly when using

Trying to determine the existence of a child, I have created a new Firebase list observable and also attempted with an object observable. Upon creating the observable, I verify if it exists or not; however, it always returns false. Database Structure: {R ...

Creating a universal timer at the application level in Angular

Is there a way to implement a timer that will automatically execute the logout() function in my authentication.service at a specific time, regardless of which page I am currently on within my application? I attempted to create a timer within my Authentica ...

What is the method to have VIM recognize backticks as quotes?

Currently working in TypeScript, I am hoping to utilize commands such as ciq for modifying the inner content of a template literal. However, it appears that the q component of the command only recognizes single and double quotation marks as acceptable ch ...

Deleting a file from the assets folder in Angular for good

I am attempting to permanently delete a JSON file from the assets folder using my component. Despite trying to use HttpClient, I encounter no errors but the file remains undeleted. constructor(http: HttpClient){} remove() { this.http.delete('assets ...

Error message in Angular 2: "__generator is not recognized"

I've been working on intercepting outgoing HTTP requests in Angular 2 in order to generate a token from the request body and attach it to the header of each post request. Below is the code snippet that I've implemented. Initially, I encountered ...

Getting a date object that is three months prior to the current date in Typescript

I need to retrieve the date object that is 3 months before the current date by using the following code snippet: toDate = new Date(); fromDate = this.toDate.getMonth() - 3; The issue I am facing is that the variable fromDate only contains a number, but I ...

Tips for maintaining the menu state following a refresh

Is there a way to save the menu state when pressing F5? I'm looking for a similar functionality as seen on the Binance website. For example, clicking on the Sell NFT's submenu and then refreshing the page with F5 should maintain the menu state on ...

Access the properties of the encapsulated component in Vue 3, allowing for IDE autocomplete support

I have a vue3 component named MyButton which acts as a wrapper for the vuetify v-btn component. I am trying to figure out a way to make MyButton props inherit all of the props that v-btn has and also enable autocomplete feature in IntelliJ or VSCode. Is it ...

Convert a regular element into a DebugElement within an Angular framework

Recently, I was working on testing an Angular Component which was going smoothly until I encountered a challenging issue that has been perplexing me for days. My main objective was to test whether the method "ajouterCompteurALaCampagne" is being called whe ...

Angular 13: Issue with displaying lazy loaded module containing multiple outlets in a component

Angular version ^13.3.9 Challenge Encountering an issue when utilizing multiple outlets and attempting to render them in a lazy module with the Angular router. The routes are being mapped correctly, but the outlet itself is not being displayed. Sequence ...

Restrict or define the acceptable values for a key within an interface

In search of defining an interface that allows for specific range of values for the key. Consider this example: interface ComparisonOperator { [operator: string]: [string, string | number]; } The key can take on values such as >, >=, !=, and so ...

What is the best way to parse JSON data with Typescript?

I am dealing with JSON data structured as follows: jsonList= [ {name:'chennai', code:'maa'} {name:'delhi', code:'del'} .... .... .... {name:'salem', code:'che'} {name:'bengaluru' ...

Sending real-time data from the tRPC stream API in OpenAI to the React client

I have been exploring ways to integrate the openai-node package into my Next.js application. Due to the lengthy generation times of OpenAI completions, I am interested in utilizing streaming, which is typically not supported within the package (refer to he ...

Error: module not found in yarn

In my yarn workspace, I have organized folders named public and server. While working with TypeScript in VS Code, I encounter an error message stating: Cannot find module 'x' Interestingly, even though the error persists, IntelliSense suggests ...

The specified type 'Observable<{}' cannot be assigned to the type 'Observable<HttpEvent<any>>'

After successfully migrating from angular 4 to angular 5, I encountered an error in my interceptor related to token refreshing. The code snippet below showcases how I intercept all requests and handle token refreshing upon receiving a 401 error: import { ...

Caution: The file path in node_modules/ngx-translate-multi-http-loader/dist/multi-http-loader.js relies on the 'deepmerge' dependency

My micro-frontend angular project is using mfe (module federation). I recently updated it from angular 13 to 14 and encountered some warnings such as: node_modules\ngx-translate-multi-http-loader\dist\multi-http-loader.js depends on ' ...

Is it possible that React.createElement does not accept objects as valid react children?

I am working on a simple text component: import * as React from 'react' interface IProps { level: 't1' | 't2' | 't3', size: 's' | 'm' | 'l' | 'xl' | 'xxl', sub ...