Typescript 中的类构造函数重载

Class constructor overloading in Typescript

提问人:D V 提问时间:11/18/2023 最后编辑:D V 更新时间:11/19/2023 访问量:63

问:

我正在开发一个类,我用它来创建两个HTMLElements:PlaybackControl

  1. PlaybackControlButton: HTMLButtonElement
  2. PlaybackControlMenu: HTMLDivElement

现在,为了初始化类对象,我需要三个参数:

  1. videoPlayer: HTMLVideoElement
  2. playbackRates: PlaybackRates
  3. options: PlaybackControlOptions

哪里:

type Shortcuts = Record<string, { key: string, value: string }>
type PlaybackRates = string[]

interface ShortcutsEnabled {
  enableShortcuts: true
  shortcuts: Shortcuts
}

interface ShortcutsDisabled {
  enableShortcuts: false
}

interface DefaultOptions extends ShortcutsEnabled {}

type PlaybackControlOptions = DefaultOptions | ShortcutsDisabled

另外,我都有所有这些的默认值,

  1. videoPlayer默认为document.querySelector('video') as HTMLVideoElement
  2. playbackRates默认为静态属性PlaybackControl.DEFAULT_PLAYBACK_RATES
  3. options默认为{ enableShortcuts: true, shortcuts: PlaybackControl.DEFAULT_SHORTCUTS }

现在,我想创建一个重载的构造函数,它应该在所有情况下都有效:

  1. 未传递任何参数
  2. 传递的参数的任意组合 任何未给出的值都应回退到其默认值,

最后,是我唯一想存储为类属性的参数,其余两个只是我只想在构造函数中调用一些函数的参数(因为我以后没有用到它们)。videoPlayer: HTMLVideoElement

目前,我编写的构造函数是:

constructor (videoPlayer?: HTMLVideoElement, playbackRates: PlaybackRates = PlaybackControl.DEFAULT_PLAYBACK_RATES, options: PlaybackControlOptions = { enableShortcuts: true, shortcuts: PlaybackControl.DEFAULT_SHORTCUTS })

虽然这确实允许我在没有任何参数的情况下进行初始化,但是当我尝试以下操作时,这失败了:

new PlaybackControl({ enableShortcuts: false })

VSCode 显示以下错误:

Object literal may only specify known properties, and 'enableShortcuts' does not exist in type 'HTMLVideoElement'.

虽然我确实了解潜在的问题(我猜),但我无法解决这个问题。 任何帮助都是值得赞赏的。

编辑:

  1. 由于口头描述可能很难深入研究,因此您可以在此处找到要运行的整个代码。

  2. 澄清一下: 我应该能够给出我想手动设置的任何参数(按顺序进行,缺少一些参数),其余的回退到默认值any combination of arguments

JavaScript TypeScript 函数 构造 函数-重载

评论

0赞 jcalz 11/18/2023
(1) 您能否提供一个最小的可重现示例,我们可以将其复制并粘贴到我们自己的 IDE 中以开始使用?现在,我们必须从口头描述中解脱出代码(例如,“我有一个名为”“它具有静态属性”的类),这使得跳入变得更加困难。让我们更轻松,您将有更高的机会获得有帮助的答案。(2)“任何论点组合”是什么意思?有订单吗?相同的订单,但缺少一些?
0赞 jcalz 11/18/2023
假设是这样,你可以尝试这个,但它既脆弱又复杂。并且对于同一类型的多个参数来说,这是不可能的。为什么不只有一个构造函数 arg 对象,比如,然后你就不必担心支持“把所有东西都扔进一个没有标签的袋子里,然后试图从袋子里捞出合适的东西”。无论如何,这是否完全解决了这个问题?如果是这样,我可以写一个答案;如果没有,我错过了什么?arg: {videoPlayer?: HTMLVideoElement, playbackRates?: PlaybackRates, options?: PlaybackControlOptions}
0赞 D V 11/18/2023
@jcalz我已经将链接添加到您可以运行的代码中。我最初计划使用单个构造函数 arg 对象,但由于我目前正在学习 Typescript,我想使用这种方法我将通过练习构造函数重载和函数签名来学习。如果这不起作用,我将使用对象方法。
0赞 D V 11/18/2023
@jcalz 非常感谢您的回答,它真的帮助了我,您的实施令人印象深刻。我同意你的两个建议,以避免麻烦,只使用一个对象。AnySubset

答:

1赞 Bergi 11/18/2023 #1

当我尝试以下操作时,这会失败:

new PlaybackControl({ enableShortcuts: false })

我不会尝试重载构造函数来接受这一点。它需要检查第一个参数是 dom 元素还是 options 对象,然后将参数洗牌到相应的变量中。这导致了相当丑陋的代码和复杂的类型签名。

相反,要跳过传递前两个参数,请传递它们,然后将选项对象作为第三个参数传递:undefined

new PlaybackControl(undefined, undefined, { enableShortcuts: false })

如果这是常见用法,请考虑将构造函数更改为仅接受一个对象参数,以便调用

new PlaybackControl({ options: { enableShortcuts: false } })

评论

0赞 D V 11/18/2023
如果参数总是以相同的顺序排列,而有些只是缺失,那么您不必打乱它们,这仍然是一个问题。我知道仅仅通过就可以解决问题,但我觉得手动通过没有意义。我更喜欢比这更好的参数对象方法。如果这不起作用,我计划使用对象方法,但由于我目前正在学习 Typescript,我想使用这种方法我将通过练习构造函数重载和函数签名来学习。undefinedundefined
0赞 Bergi 11/18/2023
是的,我假设了固定的顺序,没有洗牌。它仍然丑陋而复杂,3 个可选参数已经制作了 8 个不同的签名。区分哪个值在何处传递(即是否跳过以及跳过了多少个值)并非易事,并且取决于具有不同类型的参数,这就是为什么这种方法通常不受欢迎的原因。不过,请参阅@jcalz在他的评论中链接的 playground 代码(它甚至没有说明重载,它立即完全进入泛型)。
0赞 D V 11/18/2023
我确实明白你的意思,但你能告诉我为什么这样的事情不起作用吗? ...依此类推,直到constructor ()constructor (videoPlayer: HTMLVideoElement)constructor (playbackRates: PlaybackRates)constructor (options: PlaybackControlOptions)constructor (videoPlayer: HTMLVideoElement, playbackRates: PlaybackRates options: PlaybackControlOptions) {}
0赞 Bergi 11/18/2023
它会起作用,只是很多“等等”:P
0赞 D V 11/18/2023
我试过了,但出现错误:此重载签名与其实现签名不兼容。所以,你能帮我使这三个情况起作用吗: 1. 没有争论 2.所有参数 3.只是选项参数。你可以在这里找到我当前的实现
2赞 jcalz 11/19/2023 #2

这里的主要问题是构造函数的实现会很疯狂,因为它在输入中搜索正确类型的属性。它本质上要求没有两个参数属于同一类型,否则就会有歧义......如果你有呢?来电者写?应将哪个参数设置为?对于编写的代码,实现可能类似于a: string, b: stringnew X("c")"c"

class PlaybackControl {
  static DEFAULT_PLAYBACK_RATES = [];
  static DEFAULT_SHORTCUTS = {};
  constructor(
    ...args: ???) {
    const a: (HTMLVideoElement | PlaybackRates | PlaybackControlOptions)[] = args;
    const videoPlayer = a.find((x): x is HTMLVideoElement =>
      x instanceof HTMLVideoElement);
    const playbrackRates = a.find((x): x is PlaybackRates =>
      Array.isArray(x) && x.every(e => typeof e === "string")
    ) ?? PlaybackControl.DEFAULT_PLAYBACK_RATES;
    const options: PlaybackControlOptions = a.find((x): x is PlaybackControlOptions =>
      x && typeof x === "object" && "enableShortcuts" in x
    ) ?? { enableShortcuts: true, shortcuts: PlaybackControl.DEFAULT_SHORTCUTS };
  }
}

这应该可以按照您喜欢的方式工作和行为,但它是如此脆弱。


无论如何,假设您确实想要这样的实现,那么仍然存在如何键入它的问题。你说输入必须是有序的,有些可能缺失,所以,例如,如果它们都存在,你就不会传入。videoPlayer: HTMLVideoElement, playbackRates: PlaybackRates, options: PlaybackControlOptionsoptionsvideoPlayer

您可以手动重载构造函数以接受每个可能的输入组合。但是我喜欢玩类型,所以让我们编写一个实用程序类型,它采用输入元组类型并生成 的每个可能的子序列并集Subsequence<T>TT

然后构造函数输入可以是:

type Subsequence<T extends any[]> =
  T extends [infer F, ...infer R] ? [F, ...Subsequence<R>] | Subsequence<R> : []

这给了

type X = Subsequence<[videoPlayer: HTMLVideoElement, playbackRates: PlaybackRates, options: PlaybackControlOptions]>;
/* type X = [] | [PlaybackControlOptions] | [PlaybackRates] | [PlaybackRates, PlaybackControlOptions] | 
  [HTMLVideoElement] | [HTMLVideoElement, PlaybackControlOptions] | [HTMLVideoElement, PlaybackRates] | 
  [HTMLVideoElement, PlaybackRates, PlaybackControlOptions] */

因此,完整的构造函数如下所示

class PlaybackControl {
  static DEFAULT_PLAYBACK_RATES = [];
  static DEFAULT_SHORTCUTS = {};
  constructor(
    ...args: Subsequence<[videoPlayer: HTMLVideoElement, playbackRates: PlaybackRates, options: PlaybackControlOptions]>
  ) {
    const a: (HTMLVideoElement | PlaybackRates | PlaybackControlOptions)[] = args;
    const videoPlayer = a.find((x): x is HTMLVideoElement =>
      x instanceof HTMLVideoElement);
    const playbrackRates = a.find((x): x is PlaybackRates =>
      Array.isArray(x) && x.every(e => typeof e === "string")
    ) ?? PlaybackControl.DEFAULT_PLAYBACK_RATES;
    const options: PlaybackControlOptions = a.find((x): x is PlaybackControlOptions =>
      x && typeof x === "object" && "enableShortcuts" in x
    ) ?? { enableShortcuts: true, shortcuts: PlaybackControl.DEFAULT_SHORTCUTS };
  }
}

它的行为符合预期。

new PlaybackControl({ enableShortcuts: false })

但是,这值得吗?几乎可以肯定不是。疯狂的实现加上疯狂的打字等于太多疯狂。做这样的事情的传统方法是采用单个类型的对象输入,以利用 JavaScript 中对象为您提供的对属性顺序的自然无动于衷。歧义消失了(例如,需要 either 或 ,并且键入非常简单。所以你应该这样做。{videoPlayer?: HTMLVideoElement, playbackRates?: PlaybackRates, options?: PlaybackControlOptions}{a?: string, b?: string}{a: "c"}{b: "c"}

Playground 代码链接