【问题标题】:Typescript: Spreading function parameters defined as tuples打字稿:定义为元组的扩展函数参数
【发布时间】:2021-02-20 14:45:39
【问题描述】:

我有一个 Typescript 项目,我在函数内部调用 Date。我想使用与 Date 重载的构造函数相同的函数参数:

interface DateConstructor {
    new(): Date;
    new(value: number | string | Date): Date;
    new(year: number, month: number, date?: number, hours?: number, minutes?: number, seconds?: number, ms?: number): Date;
    // ...
}

我想使用扩展运算符将参数传递给Date 的构造函数。所以我的函数应该是这样的:

function myFn(...args: DateComponents): Date {
    return new Date(...args)
}

在我的情况下,我实际上并没有返回日期,但这只是为了测试......

现在的问题是如何定义DateComponents。我成功地分别实现了DateConstructor的3个重载签名。

// Case A: No argument provided
interface A {
  (): Date;
}
const a: A = function (...args: []): Date {
  return new Date(...args);
}

// Case B: 1 argument provided
interface B {
  (value: number | string | Date): void;
}
const b: B = function (...args: [number | string | Date]): Date {
  return new Date(...args);
}

// Case C: Between 2 and 7 arguments provided
interface C {
  (year: number, month: number, date?: number, hours?: number, minutes?: number, seconds?: number, ms?: number): void;
}
const c: C = function (...args: [number, number, number?, number?, number?, number?, number?]): Date {
  return new Date(...args);
}

到目前为止一切顺利,一切正常。

  • 答:空元组用于覆盖不传递参数的情况
  • B:一个元素的元组涵盖了传递单个数字、字符串或Date对象的情况
  • C: 包含 2 个强制和 5 个可选数字元素的元组涵盖了传递 2-7 个参数的情况

如果我尝试将这 3 个示例合并为 1 个,我会得到:

interface D {
  (): void;
  (value: number | string | Date): void;
  (year: number, month: number, date?: number, hours?: number, minutes?: number, seconds?: number, ms?: number): void;
}
type DateComponents = [] | [number | string | Date] | [number, number, number?, number?, number?, number?, number?];
const d: D = function (...args: DateComponents): Date {
  return new Date(...args); // <-- ERROR: Expected 0-7 arguments, but got 0 or more.
}

我收到一条错误消息,指出 Typescript 认为可以将超过 7 个参数传递给 Date 构造函数。我不明白为什么,因为我的类型 DateComponents 明确定义了 0-7 个元素的元组。

我可以通过使用一些条件和类型断言来解决这个问题,但我希望有一个更漂亮的解决方案?

See the code in the Playground here

有办法解决这个问题吗?非常感谢!

【问题讨论】:

  • 类型断言是最简单的解决方案

标签: typescript


【解决方案1】:

根本原因是 TypeScript 不支持同时解析对具有多个调用/构造签名的重载函数/构造函数的调用。调用new Date(...args); 是一次调用,但为了被接受,编译器必须将DateComponents 分解为其联合成员,并确保每个成员都可分配给至少一个构造签名。相反,它发现没有单个Date 构造签名适用于整个DateComponents 联合,因此放弃了。

请注意,您看到的错误的具体措辞有点牵强附会;编译器无法接受输入,并试图将其塞入有关参数数量的可用错误消息中。以前发生过这种情况(例如,microsoft/TypeScript#28010microsoft/TypeScript#20372),但它似乎并不是一个优先解决的问题。

无论如何,GitHub 中有一个(相当长期的)开放功能请求,要求重载函数接受参数联合;见microsoft/TypeScript#14107。目前尚不清楚这是否会发生。


那么,你能做什么?最简单的就是使用type assertion

const d: D = function (...args: DateComponents): Date {
  return new Date(...args as ConstructorParameters<typeof Date>);
}

我知道你想要一些“更漂亮”的东西,但相信我,我能想到的任何修复它的方法都会变得更丑。


例如,您可以手动引导编译器通过不同的可能性,并在其中放置一堆冗余代码:

const e: D = function (...args: DateComponents): Date {
  return args.length === 0 ? new Date(...args) :
    args.length === 1 ? new Date(...args) :
      new Date(...args);
}

这也不漂亮。


或者您可以尝试构建一些代码,将重载的构造函数转换为采用联合类型参数的非重载构造函数。即manually simulate an implementation of microsoft/TypeScript#14107。如果您这样做,调用将如下所示:

const f: D = function (...args: DateComponents): Date {
  return new (unifyConstructorOverloads(Date))(...args);
}

这本身并不难看。但是unifyConstructorOverloads 的定义是这样的:

type UnifyConstructorOverloads<T extends new (...args: any) => any> =
  new (...args: ConstructorParameters<ConstructorOverloads<T>[number]>) =>
    InstanceType<ConstructorOverloads<T>[number]>;

const unifyConstructorOverloads = <T extends new (...args: any) => any>(f: T) => f as
  UnifyConstructorOverloads<T>;

越来越简单,使用类型断言,并依赖于ConstructorOverloads&lt;T&gt; 的定义,这是一个假设的类型函数,它接受一个重载的构造函数并将其多个构造签名分成一个元组。据我所知,没有编程方法可以做到这一点,所以你必须模拟到一定数量的重载(比如 5 个):

type ConstructorOverloads<T> =
  T extends {
    new(...args: infer A1): infer R1; new(...args: infer A2): infer R2;
    new(...args: infer A3): infer R3; new(...args: infer A4): infer R4;
    new(...args: infer A5): infer R5;
  } ? [
    new (...args: A1) => R1, new (...args: A2) => R2,
    new (...args: A3) => R3, new (...args: A4) => R4,
    new (...args: A5) => R5
  ] : T extends {
    new(...args: infer A1): infer R1; new(...args: infer A2): infer R2;
    new(...args: infer A3): infer R3; new(...args: infer A4): infer R4
  } ? [
    new (...args: A1) => R1, new (...args: A2) => R2,
    new (...args: A3) => R3, new (...args: A4) => R4
  ] : T extends {
    new(...args: infer A1): infer R1; new(...args: infer A2): infer R2;
    new(...args: infer A3): infer R3
  } ? [
    new (...args: A1) => R1, new (...args: A2) => R2,
    new (...args: A3) => R3
  ] : T extends {
    new(...args: infer A1): infer R1; new(...args: infer A2): infer R2
  } ? [
    new (...args: A1) => R1, new (...args: A2) => R2
  ] : T extends {
    new(...args: infer A1): infer R1
  } ? [
    new (...args: A1) => R1
  ] : any

就美学而言,它远远超过“丑陋”,可能徘徊在“怪诞”周围。如果您要在代码库的许多地方执行这种重载统一的事情,我可以想象将ConstructorOverloads(以及类似的Overloads 用于常规函数,请参阅this question 以获取该代码)变成一个没有灯光的图书馆,这样您就可以使用它,而无需直视其可恶的面貌。


但如果你只做几次,我强烈建议使用类型断言并继续。

Playground link to code

【讨论】:

  • 感谢 jcalz 的精彩回答。误导性的错误信息让我认为一个简单的解决方案是可能的。我将使用类型断言,因为替代方案是......正如你所说......怪诞:)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2020-11-12
  • 1970-01-01
  • 2020-06-02
  • 2018-02-23
  • 2023-01-17
  • 2021-09-06
  • 1970-01-01
相关资源
最近更新 更多