I am working on a class PlaybackControl
, I use it to create two HTMLElements:
PlaybackControlButton: HTMLButtonElement
PlaybackControlMenu: HTMLDivElement
Now, to initialise the class object, I need three arguments:
videoPlayer: HTMLVideoElement
playbackRates: PlaybackRates
options: PlaybackControlOptions
where:
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
Also, I have default values for all of them,
videoPlayer
default todocument.querySelector('video') as HTMLVideoElement
playbackRates
defaults to a static attributePlaybackControl.DEFAULT_PLAYBACK_RATES
options
defaults to{ enableShortcuts: true, shortcuts: PlaybackControl.DEFAULT_SHORTCUTS }
Now, I want to create an overloaded constructor which should work in all cases:
- No arguments passed
- Any combination of arguments passed
Any value not given should fallback to its default,
Lastly, videoPlayer: HTMLVideoElement
is the only argument which I want to store as a class attribute, rest two are just arguments which I just want to some function calls in the constructor (because I have no later use for them).
Currently, the constructor that I wrote is:
constructor (videoPlayer?: HTMLVideoElement, playbackRates: PlaybackRates = PlaybackControl.DEFAULT_PLAYBACK_RATES, options: PlaybackControlOptions = { enableShortcuts: true, shortcuts: PlaybackControl.DEFAULT_SHORTCUTS })
while this does allow me to initialise without any argument but this fails when I try to:
new PlaybackControl({ enableShortcuts: false })
and VSCode shows error that:
Object literal may only specify known properties, and 'enableShortcuts' does not exist in type 'HTMLVideoElement'.
while I do understand the underlying problem (I guess), I am unable to resolve this.
Any help is appreciated.
Edit:
-
Since, verbal descriptions might be hard to dive in, here you can find the entire code to run.
-
By
any combination of arguments
clarification:
I should be able to give whatever argument I want to set manually (in order with some missing) and the rest fallback to default
2
Answers
I would not attempt to overload the constructor to accept this. It would need to check whether the first argument is a dom element or an options object, and then shuffle the arguments into the respective variables. This leads to rather ugly code and complicated type signatures.
Rather, to skip passing the first two arguments, pass
undefined
for them, then pass your options object as the third argument:If this is a common usage, consider changing your constructor to only accept one single object parameter, so that you’d call
The main problem here is that your implementation of the constructor will be crazy, as it searches through the inputs for the properties of the right types. It essentially requires that no two parameters are of the same type, since otherwise there’s an ambiguity… what if you had
a: string, b: string
? And the caller writesnew X("c")
? Which argument should be set to"c"
? For your code as written the implementation might be something likeThat should work and behave how you like, but it’s so fragile.
Anyway, assuming you do want such an implementation, there remains the question of how to type it. You say the input must be
videoPlayer: HTMLVideoElement, playbackRates: PlaybackRates, options: PlaybackControlOptions
in order with some possibly missing, so, for example, you’re not going to pass inoptions
beforevideoPlayer
if they both exist.You could just manually overload the constructor to accept every possible input combination. But I like playing with types, so let’s write a
Subsequence<T>
utility type that takes an input tuple typeT
and produces a union of every possible subsequence ofT
.Then the constructor input could be:
That gives
And thus the full constructor looks like
And it behaves as desired.
But, is it worth it? Almost certainly not. Crazy implementation plus crazy typings equals too much crazy. The conventional way to do something like this is to take a single object input of type
{videoPlayer?: HTMLVideoElement, playbackRates?: PlaybackRates, options?: PlaybackControlOptions}
to take advantage of the natural indifference to property order that objects in JavaScript give you. The ambiguity goes away (e.g.,{a?: string, b?: string}
would require either{a: "c"}
or{b: "c"}
, and the typing is very straightforward. So you should do that instead.Playground link to code