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

The process of importing does not produce the anticipated System.register

I'm a beginner with Angular2, currently learning and practicing by doing exercises. I am enrolled in a Udemy course and comparing my exercise progress with the instructions provided. Here is a snippet from my app.component.ts file: import {Component ...

Positioning the box at the exact middle of the screen

I'm trying to center an item on the screen using material ui, but it's appearing at the top instead of the middle. How can I fix this? import * as React from 'react'; import Box, { BoxProps } from '@mui/material/Box'; functio ...

Is there a way to access the final child element within a mat-accordion component using Material-UI and Angular 8?

I have a mat-accordion element with multiple expansion panels that are generated dynamically. How can I programmatically select and expand the last mat-expansion-panel element? <mat-accordion> <mat-expansion-panel> text 0 </mat-ex ...

A guide to mocking Prisma using Jest mock functionality

Utilizing prisma for database interactions and eager to implement jest-mock to simulate the findMany call. https://jestjs.io/docs/jest-object#jestmockedtitem-t-deep--false brands.test.ts import { PrismaService } from "@services/mysql.service"; i ...

I keep encountering the issue where nothing seems to be accessible

I encountered an error while working on a project using React and Typescript. The error message reads: "export 'useTableProps' (reexported as 'useTableProps') was not found in './useTable' (possible exports: useTable)". It ...

There was an issue while attempting to differentiate '[object Object]'. Ionic only allows arrays and iterables for this operation

I am looking for a way to extract all the "friend" objects from a JSON response and store them in an array so that I can iterate through them on an HTML webpage. ...

Outdated compiler module in the latest version of Angular (v13)

After upgrading to Angular 13, I'm starting to notice deprecations in the usual compiler tools used for instantiating an NgModule. Below is my go-to code snippet for loading a module: container: ViewContainerRef const mod = this.compiler.compi ...

Using Angular to access HTML content through the .ts file

Is there a way to retrieve the value of the input field [newUser] when clicking on the button and executing the action [onAddUser()] in the .ts file? <input type="text" ng-model="newUser" style="text-align:center"/> <button (cl ...

Tips for invoking a function from one React component to another component

Currently, I am working on two components: one is Game and the other is PickWinner. The Game component serves as the parent component, from which I need to call the pickWinner function in the PickWinner component. Specifically, I want to trigger the startP ...

Defining a JSON file interface in Angular to populate a dropdown box with dependencies

I've embarked on an exciting project to develop a cascading dropdown box filter, and it's proving to be quite challenging. I'm taking it step by step to ensure clarity. I have obtained a JSON file containing the data required to populate de ...

What is the process for transferring a function to reducers in Redux Toolkit?

In one of my files called Main.tsx, I have a function that sends a request and retrieves data: async function fetchProducts(productsPage = 1, id?: number) { const itemsPerPage = 5 let url: string if (id) { url = `https://reqres.in/api/ ...

Supplier for a module relying on data received from the server

My current component relies on "MAT_DATE_FORMATS", but I am encountering an issue where the "useValue" needs to be retrieved from the server. Is there a way to make the provider asynchronous in this case? export const MY_FORMATS = { parse: { d ...

Issue with loading React Router custom props array but custom string works fine

I am facing an issue with my ReactTS-App where I pass a prop via Router-Dom-Props to another component. The problem arises when I try to use meal.food along with meal.name, or just meal.food alone - it doesn't work as expected. Uncaught TypeError: mea ...

Patience is key as you await the completion of an API call in Angular 14

Within my Angular 14 application, I am faced with a scenario where I need to make two API calls and then combine the results using "combineLatest" from rxjs. The combined data is then assigned to a variable that I must use in a separate function. How can I ...

Creating a type or interface within a class in TypeScript allows for encapsulation of

I have a situation where I am trying to optimize my code by defining a derivative type inside a generic class in TypeScript. The goal is to avoid writing the derivative type every time, but I keep running into an error. Here is the current version that is ...

I encountered a TS error warning about a possible null value, despite already confirming that the value

In line 5 of the script, TypeScript raises an issue regarding the possibility of gameInstanceContext.gameInstance being null. Interestingly, this concern is not present in line 3. Given that I have verified its existence on line 1, it is perplexing as to w ...

The conditional type in TypeScript is malfunctioning

Upon finishing an article discussing conditional types in TypeScript located at: I have attempted to implement a conditional type in the following function: function convertToIsoString<T extends number|undefined>( timestamp:T ): T extends number ...

What steps are involved in launching an outdated Angular project?

Tasked with reviving an old Angular client in my company, I found myself grappling with outdated files and missing configurations. The lack of package.json, package-lock.json, and angular.json added to the confusion, while the presence of node modules in t ...

The color syntax in the text editor of Visual Studio 2022 is being lost when casting an interface

After attempting to cast an interface, the entire code turns white. let object : someInterface = <someInterface> someUnknownHapiRequestPayload View a screenshot of the text editor here I have already tried common troubleshooting steps such as updat ...

"Experiencing sluggish performance with VSCode while using TypeScript and Styled Components

My experience with vscode's type-checking is frustratingly slow, especially when I am using styled components. I have tried searching for a solution multiple times, but have only come across similar issues on GitHub. I attempted to read and understa ...