I am working on a class PlaybackControl, I use it to create two HTMLElements:
PlaybackControlButton: HTMLButtonElementPlaybackControlMenu: HTMLDivElement
Now, to initialise the class object, I need three arguments:
videoPlayer: HTMLVideoElementplaybackRates: PlaybackRatesoptions: 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,
videoPlayerdefault todocument.querySelector('video') as HTMLVideoElementplaybackRatesdefaults to a static attributePlaybackControl.DEFAULT_PLAYBACK_RATESoptionsdefaults 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 argumentsclarification: I should be able to give whatever argument I want to set manually (in order with some missing) and the rest fallback to default
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: PlaybackControlOptionsin order with some possibly missing, so, for example, you're not going to pass inoptionsbeforevideoPlayerif 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 typeTand 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