Get return type of class method via method name in Typescript

Solution 1:

Afaik ,there is no safe way to do what you want without changing function body or using type assertion.

In order to validate function arguments, first of all we need to obtain all method keys from Foo:

class Foo {
    var1: string = 'var1';
    var2: string = 'var2';

    hello(request: string) { }

    world(request: number) { }
}

// This type reflects any function/method
type Fn = (...args: any[]) => any

type ObtainMethods<T> = {
    [Prop in keyof T]: T[Prop] extends Fn ? Prop : never
}[keyof T]


//  "hello" | "world"
type AllowedMethods = ObtainMethods<Foo>

Let's test it:


const executeFoo = <Method extends ObtainMethods<Foo>>(
    methodName: Method
) => { }

executeFoo('hello') // ok
executeFoo('world') // ok
executeFoo('var1') // expected error

However, there is a problem with second argument:

const executeFoo = <Method extends ObtainMethods<Foo>>(
    methodName: Method, parameter: Parameters<Foo[Method]>[0]
) => {
    // Argument of type 'string | number' is not assignable to parameter of type 'never'. Type 'string' is not assignable to type 'never'.
    foo[methodName](parameter)
}

As you might have noticed, there is an error.

Argument of type 'string | number' is not assignable to parameter of type 'never'. 
Type 'string' is not assignable to type 'never'.

It is very important. If you try to call foo[methodName]() you will see that this function expects never as a type for first argument. This is because

Likewise, multiple candidates for the same type variable in contra-variant positions causes an intersection type to be inferred.

You can find more in my article, in the first part. This is because TS does not know which methodName you are using exactly. Hence, TS compiler intersects all parameters from methods: string & number because this is the only safe way to make function signature safe.

SO, it is very important what type of argument are you expect in your methods.

How to fix it ?

In this particular example, I believe using type assertion is justified:


const executeFoo = <Method extends ObtainMethods<Foo>>(
    methodName: Method, parameter: Parameters<Foo[Method]>[0]
) => {
    (foo[methodName] as (arg: Parameters<Foo[Method]>[0]) => void)(parameter)
}

executeFoo('hello', 'str') // ok
executeFoo('world', 42) // ok
executeFoo('world', "42") // expected error
executeFoo('var1') // expected error

Playground

If you are interested in function argument inference you can check my blog

It is also possible to use conditional statement for type narrowing (works in TS >= 4.6)

type Fn = (...args: any[]) => any

type ObtainMethods<T> = {
    [Prop in keyof T]: T[Prop] extends Fn ? Prop : never
}[keyof T]


//  "hello" | "world"
type AllowedMethods = ObtainMethods<Foo>

type Values<T> = T[keyof T]

type AllowedArguments = {
    [Method in AllowedMethods]: [Method, Parameters<Foo[Method]>[0]]
}

const foo = new Foo();

const executeFoo = (
    ...[name, arg]: Values<AllowedArguments>
) => {
    if (name === 'hello') {
        foo[name](arg)
    } else {
        foo[name](arg)
    }
}

executeFoo('hello', 'str') // ok
executeFoo('world', 42) // ok
executeFoo('world', "42") // expected error
executeFoo('var1') // expected error

but it does not make much sense.