打字稿:扩展函数参数定义为元组

时间:2020-11-08 13:50:01

标签: typescript

我有一个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);
}

到目前为止,一切正常。

  • A:空的元组用于覆盖没有传递参数的情况
  • B:具有一个元素的元组涵盖了传递单个数字,字符串或Date对象的情况
  • C:具有2个必需数字元素和5个可选数字元素的元组涵盖了传递2-7个参数的情况

如果我尝试将这三个示例合并为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

有解决此问题的主意吗?非常感谢!

1 个答案:

答案 0 :(得分:3)

根本原因是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<T>的定义,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 是一种假设类型函数,采用重载的构造函数并将其多个构造签名分离为元组。据我所知,没有编程的方式可以做到这一点,因此您必须模拟这种过载,直到达到一定数量的重载(例如5):

ConstructorOverloads
就美学而言,

远远超过了“丑陋”,可能徘徊在“怪诞”周围。如果您打算在代码库中的许多地方进行这种重载统一的事情,我可以想象一下,将Overloads(以及常规功能的类似HTML)关闭,请参见this question将该代码保存到一个没有光照的库中,这样您就可以使用它而无需直接查看其可恶的面貌。


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

Playground link to code