【问题标题】:Type of all overload parameter types in TypeScriptTypeScript 中所有重载参数类型的类型
【发布时间】:2021-05-17 07:35:17
【问题描述】:

考虑以下示例:

class A {
  private constructor(public n: number) {}
  public getDouble() {
    return this.n * 2;
  }
  static from(s: string): A;
  static from(n: number): A;
  static from(n1: number, n2: number): A;
  static from(...args: unknown[]): A {
    if (args.length === 1) {
      if (typeof args[0] === 'string') {
        if (args[0].length !== 1) {
          throw new Error('String must have a length of 1')
        }
        return new A(Number(args[0]));
      } else if (typeof args[0] === 'number') {
        if (args[0] > 9) {
          throw new Error('Number must be lower than 10')
        }
        return new A(args[0]);
      }
    } else if (args.length === 2) {
      if (typeof args[0] === 'number' && typeof args[1] === 'number') {
        const sum = args[0] + args[1];
        if (sum > 9) {
          throw new Error('Sum of numbers must be lower than 10')
        }
        return new A(sum);
      }
    }
    throw new Error('No overload matched')
  }
}

A 的实例只能使用静态方法from 构造,该方法被重载以获取字符串、数字或两个数字。有效参数仅包括长度为 1 的字符串、小于 10 的数字以及总和小于 10 的数字对 - 其他会导致错误。

我想实现另一个静态方法validate,它将检查是否可以从给定的参数列表构造A 的实例,如下所示:

static validate(...args: Parameters<typeof A.from>): boolean {
  try {
    A.from(...args);
    return true;
  } catch (error) {
    return false;
  }
}

问题在于 Parameters&lt;typeof A.from&gt; 解析为列表中最后一个重载的参数,在本例中为 n1: number, n2: number,这导致 TypeScript 不允许使用任何其他重载签名进行调用,即 A.validate('foo')

如何让 TypeScript 理解 validate 可以使用与 from 相同的重载签名来调用?

我找到了两种可能的解决方案,但都有很大的缺点:

  1. 我可以手动构造另一个与之前所有重载匹配的重载签名并将其放在列表的末尾。但是,这将很难维护,并且会与 IntelliSense 提示混淆。
  2. 类似于 1.,但签名将允许 ...args: unknown[]。然而,这会使 TypeScript 认为 from 实际上可以用任何参数调用,而实际上它不能。
  3. 我可以复制并粘贴所有from 重载签名,并将它们声明为validate。同样,难以维护 - 实际用例涉及更多的重载以及不止一个模仿 from 重载的静态方法,这将导致 50 多行重载声明。

还有其他方法可以解决这个问题吗?有没有办法写一个OverloadParameters 类型?

【问题讨论】:

    标签: typescript


    【解决方案1】:

    如果你有一个重载的函数/方法,其中每个调用签名都有相同的返回类型(就像from() 的情况,返回类型总是A),你可以将该函数重构为具有 single 调用签名的版本,该调用签名具有类型为 unionrest tuples 的 aa union 的其余参数:

    static from(...args: [s: string] | [n: number] | [n1: number, n2: number]): A {
      // same impl as before
    }  
    

    这和以前一样可以调用:

    A.from("okay"); // works
    A.from(1); // okay
    A.from(2, 4); // okay
    A.from("oops", 2); // error!
    

    实际上,从 IntelliSense 的角度来看,它甚至看起来像是一个重载函数:

    /* A.from(⎀) */
    // 1/3 from(s: string): A
    // 2/3 from(n: number): A
    // 3/3 from(n1: number, n2: number): A
    

    但至关重要的是,Parameters&lt;typeof A["from"]&gt; 现在是元组的完整并集,不会丢失任何信息:

    static validate(...args: Parameters<typeof A.from>): boolean {
      // same impl as before
    }
    

    并且您可以验证A.validate() 可以以与A.from() 完全相同的方式调用:

    A.validate("okay"); // works
    A.validate(1); // okay
    A.validate(2, 4); // okay
    A.validate("oops", 2); // error!
    

    Playground link to code

    【讨论】:

      【解决方案2】:

      我找到了基于解决方案 3 的解决方法。我试图弄清楚是否有一种方法可以声明共享相同重载列表的多个成员。它不漂亮,但我不相信有更优雅的方式来处理这个问题。

      解决方案涉及将类转换为类型化的类表达式并为重载编写泛型类型。继续问题中的示例,重载类型如下所示:

      type MyOverload<ReturnType> = {
        (s: string): ReturnType;
        (n: number): ReturnType;
        (n1: number, n2: number): ReturnType;
      }
      

      然后我们为类及其构造函数声明一个接口:

      interface AInterface {
        getDouble(): number;
      }
      
      interface AConstructor {
        new (n: number): AInterface;
        from: MyOverload<AInterface>;
        validate: MyOverload<boolean>;
      }
      

      最后是类实现,没有声明重载:

      const A: AConstructor = class A implements AInterface {
        constructor(public n: number) {}
        static from(...args: unknown[]): A {
          // same as before
        }
        static validate(...args: unknown[]): boolean {
          try {
            A.from(...args);
            return true;
          } catch (error) {
            return false;
          }
        }
      }
      

      这实现了什么:

      • 您可以声明多个具有相同重载参数列表但返回类型不同的方法。
      • 他们的调用签名将被正确输入并与 IntelliSense 一起使用。

      限制(据我所知):

      • 您需要在接口中声明类的公共端,然后在类中重新声明并实现它,并使这两个声明保持同步。这对于重载方法来说显然是一个优势,但对于其他方法来说就没有那么多了,而且会成为维护的负担。
      • 没有受保护的成员,既不是实例也不是静态的(因为接口和类型不能声明私有/受保护成员)。
      • 为重载的方法提供额外的参数会很麻烦。

      您可以进一步概括重载类型以采用额外的参数或多个参数(即对于像 createAndMultiplyBy 这样的方法):

      type MyOverload<ReturnType, MethodArg = never> = {
        (s: string, arg: MethodArg): ReturnType;
        (n: number,  arg: MethodArg): ReturnType;
        (n1: number, n2: number, arg: MethodArg): ReturnType;
      }
      

      然后将方法声明为createAndMultiply: MyOverload&lt;number, number&gt;。但是,额外的参数不会得到名称,如果有多个(即type MyOverload&lt;ReturnType, MethodArgs extends any[] = never&gt;),那么 IntelliSense 必须将它们显示为MyOverload&lt;any, [number, number]&gt; - (s: string, ...args: [number, number])

      对于我的用例,这些额外的重载方法应该只是额外的一层糖化实用程序。我认为维护这些额外接口的开销不值得,因此放弃了实现它们。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2019-07-23
        • 1970-01-01
        • 2022-11-16
        • 2016-11-01
        • 2018-08-28
        • 2021-07-10
        • 2021-04-23
        • 1970-01-01
        相关资源
        最近更新 更多