Home > Mobile >  Typescript implementing a generic function that accept multiple type of inputs
Typescript implementing a generic function that accept multiple type of inputs

Time:10-20

For abstraction purposes I need to implement a function that accept different types of inputs.

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 input is needed for a different method:

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

Now I need to implement a generic function that accept all of the inputs, this is because the input types can be a lot, now there are only 2, but in my real application they are around 16.

I would like to have an implementation like this one, however it lead me to 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 infer that since I am using input.name then the argument of the method is correct - since they will always match input.name and input.content?

Playground link

CodePudding user response:

First, be aware that the code in the playground is quite different from the one in the question.

The issue with this code is that you're not effectively narrowing the type / one way to do it effectively would be by using a switch statement:

function foo(input: Input) {
    switch(input.name) {
        case 'method_a': {
            // input.content is guaranteed to be of type string
            console.log(input.content)
            break;
        }
        case 'method_b': {
            // input.content is guaranteed to be of type number
            console.log(input.content)
            break;
        }
    }
}

CodePudding user response:

There are different solutions I can propose one of them:

Working 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`);
    }
}

CodePudding user response:

I don't think this can be done, because what would the type of my_methods[input.name] be? It cannot be determined at compile time more strictly than (ContentA) => void | (ContentB) => void.

So you need a cast:

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

Other answers propose solutions without a cast, which rely on control flow to do type narrowing. Those approaches are also valid, of course, and which is better depends on the situation and on preference.

One improvement you can make if you stick with your current approach, though, is to type my_methods more strictly, so that the compiler can check that the keys and argument types actually match the possible 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) => {
        // ...
    }
    
}
  • Related