代理类方法时类型无法匹配的问题

经常会有需求对一个对象/类的方法做代理调用,比如如下对象:

const fns = {
    say: (msg: string) => msg,
    sum: (a: number, b: number) => a + b
}

我们期望通过类似 call('methodName', ...paramas) 的方式实现代理调用,很自然的会写出如下的代码:

type FNS = typeof fns;

function call<T extends keyof FNS>(fn: T, ...args: Parameters<FNS[T]>) {
     fns[fn].apply(fns, args)
}

然而,会报如下类型错误:

The 'this' context of type '((msg: string) => string) | ((a: number, b: number) => number)' is not assignable to method's 'this' of type '(this: { say: (msg: string) => string; sum: (a: number, b: number) => number; }, msg: string) => string'.
  Type '(a: number, b: number) => number' is not assignable to type '(this: { say: (msg: string) => string; sum: (a: number, b: number) => number; }, msg: string) => string'.(2684)

原因是 fns[fn] 无法与 args 匹配上。

实际上我们在参数定义上的类型声明已经绑定了 fnargs 的关系(调用 call 时可正常推断类型了),但在函数实现部分将 fn 倒腾为了 fns[fn],对编译器而言,fn 是类型 T extends keyof FNS,那 fns[fn] 自然就对应了 fns 的所有方法了,无法再与 args 产生一对一的绑定关系。

解决方案如下:

function callFn<T extends (...args: any[]) => any>(f: T, args: Parameters<T>): ReturnType<T> {
  return f(...args)
}

function call<T extends keyof FNS>(fn: T, ...args: Parameters<FNS[T]>) {
     callFn(fns[fn], args);
}