最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Get return type of class method via method name in Typescript - Stack Overflow

programmeradmin1浏览0评论

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.

Share Improve this question edited Jan 11, 2022 at 10:59 Parzh 9,9534 gold badges46 silver badges80 bronze badges asked Jan 10, 2022 at 12:22 PaosderPaosder 291 silver badge6 bronze badges 3
  • 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
Add a ment  | 

2 Answers 2

Reset to default 8

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 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);
}
发布评论

评论列表(0)

  1. 暂无评论