TypeScript方法来包装另一个类的任何方法

huangapple go评论53阅读模式
英文:

TypeScript method to wrap any method of another class

问题

抽象类 Test 中的 return 行出现了两个错误:

  1. 错误1:类型 'number | Promise' 无法赋值给类型 'ReturnType<Test[K]>'。
  2. 错误2:类型 'number' 无法赋值给类型 'ReturnType<Test[K]>'。
  3. 错误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&lt;number&gt;;
}

class Wow {
    constructor(private test: Test) {}

    public fetch&lt;K extends keyof Test&gt;(key: K, params: Parameters&lt;Test[K]&gt;): ReturnType&lt;Test[K]&gt; {
        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 没有指定为联合),但编译器无法看到这一点。它已经失去了 keyparams 之间的 相关性。这里的一般问题是 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]
}

TestParamsTestReturn 类型是 "基本的键值接口类型",而 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&lt;T&gt; and ReturnType&lt;T&gt; utility types are implemented as conditional types, and thus Parameters&lt;Test[K]&gt; and ReturnType&lt;Test[K]&gt; 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&lt;Test[K]&gt; };
type TestReturn = { [K in keyof Test]: ReturnType&lt;Test[K]&gt; };
type TestMapped = { [K in keyof Test]:
  (...args: TestParams[K]) =&gt; 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&lt;K extends keyof Test&gt;(
    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]) =&gt; 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 &#39;[TestParams[K]]&#39; is not assignable to parameter of type &#39;TestParams[K]&#39;, which hopefully would be enough information for you to fix the problem.

Now everything works as desired.

Playground link to code

答案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&lt;number&gt;;
  public abstract foo: () =&gt; number;
  public abstract bar: number;
}

class Wow {
  constructor(private test: Test) { }

  public fetch&lt;K extends MethodKeys&lt;Test&gt;&gt;(key: K, ...params: Parameters&lt;Test[K]&gt;): ReturnType&lt;Test[K]&gt; {
    return (this.test[key] as (...a: any[])=&gt;any)(...params);
  }
  // if you want arglist param
  public fetchA&lt;K extends MethodKeys&lt;Test&gt;&gt;(key: K, params: Parameters&lt;Test[K]&gt;): ReturnType&lt;Test[K]&gt; {
    return (this.test[key] as (...a: any[])=&gt;any)(...params);
  }
}

let x = new Wow(new Test())
x.fetch(&#39;method1&#39;, &#39;aa&#39;)
x.fetch(&#39;method2&#39;, 123, &#39;ss&#39;);
x.fetch(&#39;foo&#39;)

// note this doesn&#39;t method vs function prop
type MethodKeys&lt;T&gt; = keyof {
  [K in keyof T as T[K] extends (...a: any[]) =&gt; any ? K : never]: 1
}

huangapple
  • 本文由 发表于 2023年7月10日 22:26:24
  • 转载请务必保留本文链接:https://go.coder-hub.com/76654728.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定