英文:
TypeScript method to wrap any method of another class
问题
抽象类 Test
中的 return
行出现了两个错误:
- 错误1:类型 'number | Promise
' 无法赋值给类型 'ReturnType<Test[K]>'。 - 错误2:类型 'number' 无法赋值给类型 'ReturnType<Test[K]>'。
- 错误3:预期 2 个参数,但只提供了 1 个。
这些错误的原因是您的 fetch
方法的实现与 TypeScript 的类型检查不匹配。为了解决这些错误,您可以按照以下方式更改您的代码:
class Wow {
constructor(private test: Test) {}
public fetch<K extends keyof Test>(key: K, ...params: Parameters<Test[K]>): ReturnType<Test[K]> {
return this.test[key](...params);
}
}
这里的关键更改是使用了 ...
操作符来展开参数,以匹配 this.test[key]
方法的期望参数。这将使您的 fetch
方法与指定的键的方法参数和返回类型保持一致,并解决了上述错误。
英文:
I have the following setup in TypeScript:
abstract class Test {
public abstract method1(param1: string): number;
public abstract method2(param1: number, param2: string): Promise<number>;
}
class Wow {
constructor(private test: Test) {}
public fetch<K extends keyof Test>(key: K, params: Parameters<Test[K]>): ReturnType<Test[K]> {
return this.test[key](params);
}
}
But the return
line inside the method fetch
gives two errors:
> Type 'number | Promise<number>' is not assignable to type 'ReturnType<Test[K]>'.
> Type 'number' is not assignable to type 'ReturnType<Test[K]>'. (2322)
> Expected 2 arguments, but got 1. (2554)
I'm puzzled on why these errors appear and how to solve them.
What I want is for my method fetch
to take a specific key of my Test
class and be strongly typed with the proper parameters and return value of method specified by the key
.
答案1
得分: 1
TypeScript 对依赖于泛型类型参数的条件类型不能进行太多推理。Parameters<T>
和ReturnType<T>
实用类型是作为条件类型实现的,因此 Parameters<Test[K]>
和 ReturnType<Test[K]>
对编译器来说基本上是不透明的。它最好能做的是将 K
扩展到它的约束,keyof Test
,所以最终得到方法名称的联合以及参数列表的联合。然后编译器变得困惑,因为它无法确定 this.test[key]
是否实际上接受 params
作为其参数列表,因为也许你将 method1
的参数传递给了 method2
或反之亦然。虽然这不太可能发生(只要 K
没有指定为联合),但编译器无法看到这一点。它已经失去了 key 和 params 之间的 相关性。这里的一般问题是 TypeScript 不直接支持我称之为 "相关联合" 的功能,如 microsoft/TypeScript#30581 中所讨论的。
该问题的推荐修复方法在 microsoft/TypeScript#47109 中有描述。编译器在处理基本的键值接口类型以及对这些类型进行泛型索引和映射类型时更加出色。
对于你的示例,这意味着我们需要将 Test
重写为这样一个映射类型。像这样:
type TestParams = { [K in keyof Test]: Parameters<Test[K]> };
type TestReturn = { [K in keyof Test]: ReturnType<Test[K]> };
type TestMapped = { [K in keyof Test]:
(...args: TestParams[K]) => TestReturn[K]
}
TestParams
和 TestReturn
类型是 "基本的键值接口类型",而 TestMapped
则是这些类型的映射类型。你可以看到类型 TestMapped
完全等同于类型 Test
,事实上编译器允许你将类型为 Test
的值分配给类型为 TestMapped
的变量。
declare const t: Test;
const tM: TestMapped = t; // 可以
现在,你可以重新编写 fetch()
:
class Wow {
constructor(private test: Test) { }
public fetch<K extends keyof Test>(
key: K, params: TestParams[K]): TestReturn[K] {
const thisTest: TestMapped = this.test;
return thisTest[key](...params);
}
}
在这里,params
输入类型和返回类型现在都写成了对我们基本键值接口类型的泛型索引。在实现内部,我们将 this.test
分配给类型为 TestMapped
的变量 thisTest
,这使编译器能够 "看到" 我们在调用 thisTest[key](...params)
时正在做什么。thisTest[key]
的类型被视为 (...args: TestParams[K]) => TestReturn[K]
,而 params
的类型被视为 TestParams[K]
,因此用前者的扩展参数列表调用后者会产生类型为 TestReturn[K]
的结果,这是函数的期望输出类型。所以一切都正常工作。
请注意,此重构捕获了一个错误:你的代码是这种形式的 thisTest[key](params)
,而不是 thisTest[key](...params)
,这意味着你将整个 params
数组作为第一个参数传递,而不是将它展开成多个参数。如果保留这种方式,你将会得到编译器错误:Argument of type '[TestParams[K]]' is not assignable to parameter of type 'TestParams[K]'
,希望这足以为你提供足够的信息来修复问题。
现在一切都按预期工作。
英文:
TypeScript can't really do much reasoning about conditional types that depend on generic type parameters. The Parameters<T>
and ReturnType<T>
utility types are implemented as conditional types, and thus Parameters<Test[K]>
and ReturnType<Test[K]>
are essentially opaque to the compiler. The best it can do is to widen K
to its constraint, keyof Test
, and so you end up with a union of method names and a union of parameter lists. And then the compiler gets confused because it can't be sure that this.test[key]
actually accepts params
as its parameter list, because maybe you're passing the method1
params to method2
or vice versa. This is unlikely to actually happen (as long as K
isn't specified with a union), but the compiler can't see that. It has lost track of the correlation between key
and params
. The general issue here is TypeScript's lack of direct support for what I call "correlated unions", as discussed in microsoft/TypeScript#30581
The recommended fix for that issue is described in microsoft/TypeScript#47109. The compiler is better about dealing with basic key-value interface types, and generic indexes into such types and mapped types over them.
For your example, it means that we need to rewrite Test
as such a mapped type. Like this:
type TestParams = { [K in keyof Test]: Parameters<Test[K]> };
type TestReturn = { [K in keyof Test]: ReturnType<Test[K]> };
type TestMapped = { [K in keyof Test]:
(...args: TestParams[K]) => TestReturn[K]
}
The TestParams
and TestReturn
types are the "basic key-value interface types", and TestMapped
the mapped type over these types. You can see that the type TestMapped
is completely equivalent to the type Test
, and indeed the compiler will allow you to assign a value of type Test
to a variable of type TestMapped
.
declare const t: Test;
const tM: TestMapped = t; // okay
And now you can rewrite fetch()
:
class Wow {
constructor(private test: Test) { }
public fetch<K extends keyof Test>(
key: K, params: TestParams[K]): TestReturn[K] {
const thisTest: TestMapped = this.test;
return thisTest[key](...params);
}
}
Here the params
input type and the return type are now written as generic indexes into our basic key-value interface types. Inside the implementation, we assign this.test
to a variable thisTest
of type TestMapped
, which enables the compiler to "see" what we're doing when we call thisTest[key](...params)
. The type of thisTest[key]
is seen to be (...args: TestParams[K]) => TestReturn[K]
, and the type of params
is seen to be TestParams[K]
, so calling the former with a spread argument list of the latter produces a result of type TestReturn[K]
, which is the desired output type of the function. So everything works.
Note that this refactoring caught an error: your code was of the form thisTest[key](params)
instead of thisTest[key](...params)
, meaning you passed the whole params
array as the first argument instead of spreading it into multiple arguments. If you left it that way you'd get the compiler error: Argument of type '[TestParams[K]]' is not assignable to parameter of type 'TestParams[K]'
, which hopefully would be enough information for you to fix the problem.
Now everything works as desired.
答案2
得分: 0
抽象类 Test {
public abstract method1(param1: string): number;
public abstract method2(param1: number, param2: string): Promise<number>;
public abstract foo: () => number;
public abstract bar: number;
}
类 Wow {
constructor(private test: Test) { }
public fetch<K extends MethodKeys<Test>>(key: K, ...params: Parameters<Test[K]>): ReturnType<Test[K]> {
return (this.test[key] as (...a: any[]) => any)(...params);
}
// 如果您想要参数列表参数
public fetchA<K extends MethodKeys<Test>>(key: K, params: Parameters<Test[K]>): ReturnType<Test[K]> {
return (this.test[key] as (...a: any[]) => any)(...params);
}
}
let x = new Wow(new Test())
x.fetch('method1', 'aa')
x.fetch('method2', 123, 'ss');
x.fetch('foo')
// 请注意,这不是方法 vs. 函数属性
类型 MethodKeys<T> = keyof {
[K in keyof T as T[K] extends (...a: any[]) => any ? K : never]: 1
}
英文:
abstract class Test {
public abstract method1(param1: string): number;
public abstract method2(param1: number, param2: string): Promise<number>;
public abstract foo: () => number;
public abstract bar: number;
}
class Wow {
constructor(private test: Test) { }
public fetch<K extends MethodKeys<Test>>(key: K, ...params: Parameters<Test[K]>): ReturnType<Test[K]> {
return (this.test[key] as (...a: any[])=>any)(...params);
}
// if you want arglist param
public fetchA<K extends MethodKeys<Test>>(key: K, params: Parameters<Test[K]>): ReturnType<Test[K]> {
return (this.test[key] as (...a: any[])=>any)(...params);
}
}
let x = new Wow(new Test())
x.fetch('method1', 'aa')
x.fetch('method2', 123, 'ss');
x.fetch('foo')
// note this doesn't method vs function prop
type MethodKeys<T> = keyof {
[K in keyof T as T[K] extends (...a: any[]) => any ? K : never]: 1
}
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论