我正在尝试实现一个“代理”方法,它能够动态调用该类的其他方法。一切都很顺利,直到我开始尝试其他方法参数的类型。以下示例给出
A spread argument must either have a tuple type or be passed to a rest parameter.
我做错了什么?根据 docs,
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 的已知错误/限制,如 microsoft/TypeScript#36874 和 microsoft/TypeScript#42508 中所述(以及其中链接的问题)。如果您尝试将某些内容分散到函数调用中,TypeScript 目前似乎只允许其类型为单个元组。不支持元组的并集,或对元组进行约束的泛型,或对元组求值的条件类型。
如果您只想继续前进并防止错误,您可以使用 类型断言:
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]
将是函数的并集。并且函数的联合只能通过其参数类型的“交集”来安全地调用。有关该功能的说明,请参阅TS3.3 发行说明。由于
methodA
想要一个 number
,而 methodB
想要一个 string
,因此您只能安全地传递 a
number
和 a string
的东西,所以哪个并不重要你调用的方法。呃,但是没有这样的事情。 number & string
被简化为不可能的
never
类型。因此,编译器唯一会让你通过的
options
是
[never]
。您可能会认为 methodName
和 options
彼此
相关,因此调用
this[methodName](...options)
总是合适的,但编译器看不到这种相关性。函数的实现具有 methodName
和 options
作为两个 独立联合类型,因此就编译器在该实现中所知,
methodName
很可能是 "methodA"
,而 options
是 [string]
。 TypeScript 实际上不直接支持相关联合,如 microsoft/TypeScript#30581中所述。 如果您希望编译器真正“理解”
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);
}
}
基本类型是MethodParam
,我们将
this
分配给映射类型
t
的变量
{ [P in keyof MethodParam]: (...args: MethodParam[P]) => void }
。编译器认为该赋值是可接受的(因为它会遍历 MethodParam
的每个属性,并可以验证 TestClass
在该属性上具有适当形状的方法)。并且 methodName
是泛型类型 MethodParam
的 K
的键。 options
是 MethodParam
、MethodParam[K]
的通用索引。现在 t[methodName]
是该映射类型的通用索引,并且可以被视为单个函数类型 (...args: MethodParam[K]) => void
。由于 options
属于
MethodParam[K]
类型,因此 t[methodName](...options)
是可接受的。不涉及函数的并集,也不需要参数的交集。对于您的用例来说,这种重构可能不值得。你可能更关心权宜之计(“停止抱怨,编译器,让我继续我的一天”)而不是保护(“我需要你理解我在做什么,编译器,这样如果我犯了一个错误,你就会抓住它”)。如果是这样,那么类型断言就是正确的选择。Playground 代码链接