Let's say we have a class:
class Foo {
var1: string = 'var1';
var2: string = 'var2';
hello(request: A): Promise<B> { }
world(request: C): Promise<D> { }
}
I want to implement the function that executes a method of the instance of Foo
:
const foo = new Foo();
const executeFoo = (methodName: string, firstParam: any) => { // <- I'm stuck in this arrow function.
return foo[methodName](firstParam);
};
executeFoo('hello', testParam); // testParams is type of A, then return type should Promise<B>.
executeFoo('world', testParam2); // testParams2 is type of C, then return type should Promise<D>.
Is there a way to define the type of executeFoo
? I'm totally confused about how to resolve this problem.
Let's say we have a class:
class Foo {
var1: string = 'var1';
var2: string = 'var2';
hello(request: A): Promise<B> { }
world(request: C): Promise<D> { }
}
I want to implement the function that executes a method of the instance of Foo
:
const foo = new Foo();
const executeFoo = (methodName: string, firstParam: any) => { // <- I'm stuck in this arrow function.
return foo[methodName](firstParam);
};
executeFoo('hello', testParam); // testParams is type of A, then return type should Promise<B>.
executeFoo('world', testParam2); // testParams2 is type of C, then return type should Promise<D>.
Is there a way to define the type of executeFoo
? I'm totally confused about how to resolve this problem.
- 2 Your code is syntactically incorrect, do you know that? – Parzh Commented Jan 10, 2022 at 12:50
- @Paosder consider publishing reproducible example without syntax error. Try to paste your code in TS playground before publishing. It will make things easier – captain-yossarian from Ukraine Commented Jan 10, 2022 at 13:38
- @DimaParzhitsky Yes, that was some of pseudo code, but I found executeFoo has totally wrong syntax. Thanks for pointing out. – Paosder Commented Jan 11, 2022 at 2:16
2 Answers
Reset to default 8Afaik ,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 piler 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.
You can pass a generic key type extending keyof Foo
to look up the method in Foo and then get the correct method signature like so:
type ArgsOf<F> = F extends (...args: infer A) => void ? A : never;
const foo = new Foo();
function runCommand<K extends keyof Foo>(name: K, ...args: ArgsOf<Foo[K]>) {
return (foo[name] as any)(...args);
}