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.