如何在“proxy”函数中推断函数参数的类型

How to infer type of function argument in "proxy" function

提问人:Marian Bazalik 提问时间:11/10/2023 更新时间:11/10/2023 访问量:28

问:

我正在尝试实现一个“代理”方法,该方法能够动态调用该类的其他方法。在我开始尝试其他方法参数之前,一切都很好。以下示例给出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);
  }
}

操场

TypeScript 参数 元组

评论

0赞 jcalz 11/10/2023
这是 TS 的 bug 或设计限制,请参阅 ms/TS#42508ms/TS#36874。如果你只想推进,你可以使用一个类型断言,如图所示,但“正确”的编写方法是使用泛型,如图所示。这是否完全解决了这个问题?如果是这样,我会写一个答案来解释;如果没有,我错过了什么?
0赞 Marian Bazalik 11/10/2023
是的,这解决了我的问题。如果你能在你的答案中花费,我会很惊讶,因为我不能说我理解它为什么有效[never]

答:

0赞 jcalz 11/10/2023 #1

你遇到了 TypeScript 的已知 bug/限制,如 microsoft/TypeScript#36874microsoft/TypeScript#42508 中所述(以及其中链接的问题)。如果你试图将一些东西传播到函数调用中,TypeScript 目前似乎只允许在其类型是单个元组时这样做。不支持元组的并集,或约束为元组的泛型,或计算结果为元组的条件类型

如果只想向前推进并防止错误,可以使用类型断言

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

因此,是单个元组类型,因此它被视为可传播的。但是为什么?这是因为编译器不知道是 is 还是 .这是两者的结合。功能的结合也是如此。函数的联合只能通过其参数类型的交集来安全地调用。有关该功能的说明,请参阅 TS3.3 发行说明。由于想要 a 和 想要 a ,你只能安全地传递同时是 a a 的东西,所以你调用哪个方法并不重要。呃,但是没有这样的事情。 被简化为不可能的永不类型。因此,编译器唯一允许您传递的就是 。[never]nevermethodName"methodA""methodB"this[methodName]methodAnumbermethodBstringnumberstringnumber & stringoptions[never]

您可能认为 并且以这样一种方式相互关联,以便始终适合调用 ,但编译器看不到这种关联。函数的实现具有 和 作为两个独立的联合类型,因此就编译器在该实现内部所知道的而言,很有可能是 while is 。TypeScript 实际上没有对相关联合的直接支持,如 microsoft/TypeScript#30581 中所述。methodNameoptionsthis[methodName](...options)methodNameoptionsmethodName"methodA"options[string]


如果希望编译器真正“理解”这始终是合适的,则需要按照 microsoft/TypeScript#47109 中所述重构代码。这个想法是远离联合(在你的情况下是重载),转向泛型。您用“基本”键值类型、该类型的泛型索引以及该类型的映射类型的泛型索引来表示操作。this[methodName](...options)

对于您的示例,它可能看起来像

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

基本类型是 ,我们分配给映射类型的变量。编译器认为该赋值是可接受的(因为它遍历了 的每个属性,并且可以验证该属性是否具有适当形状的方法)。并且是泛型类型的键。并且是 、 的通用索引。MethodParamthist{ [P in keyof MethodParam]: (...args: MethodParam[P]) => void }MethodParamTestClassmethodNameMethodParamKoptionsMethodParamMethodParam[K]

现在是该映射类型的泛型索引,可以看作是单函数类型。由于 是 类型,则是可以接受的。不涉及函数的并集,也不需要参数的交集。t[methodName](...args: MethodParam[K]) => voidoptionsMethodParam[K]t[methodName](...options)

对于您的用例来说,这种重构可能不值得。你可能更关心权宜之计(“别抱怨了,编译器,让我继续我的一天”),而不是保护(“我需要你明白我在做什么,编译器,这样如果我犯了一个错误,你就会抓住它”)。如果是这样,那么类型断言就是要走的路。

Playground 代码链接