通过 Typescript 中的方法名称获取 return 类型的 class 方法

Get return type of class method via method name in Typescript

假设我们有一个 class:

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

  hello(request: A): Promise<B> {  }

  world(request: C): Promise<D> {  }
}

我想实现执行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>.

有没有办法定义executeFoo的类型?我完全不知道如何解决这个问题。

Afaik,没有 安全 方法可以在不更改函数体或使用类型断言的情况下做你想做的事。

为了验证函数参数,首先我们需要从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>

让我们测试一下:


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

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

但是,第二个参数有问题:

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)
}

您可能已经注意到,出现错误。

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

非常重要。如果您尝试调用 foo[methodName](),您将看到此函数期望 never 作为第一个参数的类型。这是因为

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

您可以在我的 article 的第一部分中找到更多信息。这是因为 TS 不知道你用的是哪个methodName。因此,TS 编译器与方法的所有参数相交:string & number 因为这是使函数签名安全的唯一安全方法。

所以,您希望在您的方法中使用哪种类型的参数非常重要。

如何解决?

在这个特定的例子中,我认为使用 type assertion 是合理的:


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

如果你对函数参数推断感兴趣,你可以查看我的 blog

也可以使用条件语句来缩小类型(适用于 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

但意义不大