typescript 如何在“代理”函数中推断函数参数的类型

fhity93d  于 12个月前  发布在  TypeScript
关注(0)|答案(1)|浏览(201)

我试图实现一个“代理”方法,它能够动态调用类的其他方法。在我开始尝试其他方法参数类型之前,一切都很好。下面的例子给出了A spread argument must either have a tuple type or be passed to a rest parameter.
我做错了什么?根据文档,Parameters实用函数返回元组:(

class testClass {
  public methodA(a: number): void {
  }

  public methodB(b: string): void {
  }

  public callRandom(methodName: 'methodA', options: [number]): void;
  public callRandom(methodName: 'methodB', options: [string]): void;
  public callRandom(methodName: 'methodA' | 'methodB', options: [number] | [string]) {
      type optionsType = Parameters<this[typeof methodName]>; 
      this[methodName](...options as optionsType);
  }
}

字符串
操场

hgtggwj0

hgtggwj01#

您遇到了TypeScript的一个已知错误/限制,在microsoft/TypeScript#36874microsoft/TypeScript#42508(以及其中链接的问题)中进行了描述。如果您试图将某些内容扩展到函数调用中,TypeScript目前似乎只允许其类型为单个元组。不支持元组的联合,或限制为元组的generics,或计算为元组的conditional types
如果你只是想继续前进并防止错误,你可以使用类型Assert:

public callRandom(methodName: 'methodA' | 'methodB', options: [number] | [string]) {
    type optionsType = Parameters<this[typeof methodName]>;
    this[methodName](...options as [never]);
  }

字符串
所以,[never]是一个单一的元组类型,所以它被看作是可扩展的。但是为什么是never呢?这是因为编译器不知道methodName"methodA"还是"methodB"。因此this[methodName]将是函数的联合。并且函数的联合只能通过其参数类型的交集来安全地调用。请参见由于methodA需要一个number,而methodB需要一个string,所以你只能安全地传递一个同时是numberstring的东西,所以你调用的是哪一个方法并不重要。但是没有这样的东西。number & string被简化为不可能的never类型。因此编译器唯一允许你传递的options[never]
你可能会认为methodNameoptions是相互关联的,所以调用this[methodName](...options)总是合适的,但是编译器看不到这种关联。你的函数的实现有methodNameoptions作为两个独立的联合类型,所以编译器在实现中知道,methodName很可能是"methodA",而options[string]。TypeScript实际上并不直接支持相关的联合,如microsoft/TypeScript#30581中所述。
如果你想让编译器真正“理解”this[methodName](...options)总是合适的,那么你需要像microsoft/TypeScript#47109中描述的那样重构你的代码。你用“基本”键值类型来表示你的操作,用泛型indexes来表示这个类型,以及对该类型的mapped types的泛型索引。
对于你的例子,它可能看起来像

interface MethodParam {
  methodA: [number],
  methodB: [string]
}

class TestClass {
  public methodA(a: number): void {
  }

  public methodB(b: string): void {
  }

  public callRandom<K extends keyof MethodParam>(
    methodName: K, 
    options: MethodParam[K]
  ) {
    const t: { [P in keyof MethodParam]: (...args: MethodParam[P]) => void } = this;
    t[methodName](...options);
  }
}


基本类型为MethodParam,我们将this赋值给Map类型为{ [P in keyof MethodParam]: (...args: MethodParam[P]) => void }的变量t。编译器认为该赋值是可接受的(因为它遍历了MethodParam的每个属性,并且可以验证TestClass在该属性处具有适当形状的方法)methodNameMethodParam的一个键,其泛型类型为KoptionsMethodParamMethodParam[K]的泛型索引。
现在t[methodName]是该Map类型的泛型索引,可以看作单个函数类型(...args: MethodParam[K]) => void。由于optionsMethodParam[K]类型,因此t[methodName](...options)是可接受的。不涉及函数的联合,也不需要参数的交集。
这种重构对于你的用例来说可能不值得。你可能更关心的是权宜之计(“停止抱怨,编译器,让我继续我的一天”)而不是保护(“我需要你理解我在做什么,编译器,这样如果我犯了错误,你会发现它”)。如果是这样,那么类型Assert是一条路要走。
Playground代码链接

相关问题