I'm new to TypeScript and I'm trying to port some of my custom components/plugins to TS.
One of the things I just can't seem to get right is setting (typed) object properties dynamically (i.e. when the property name is a variable).
A best-practice solution/pattern for this would really help me along.
My code:
interface Options {
repeat: boolean;
speed: number;
}
class MyPlugIn {
$el:HTMLElement;
options:Options;
constructor ($el:HTMLElement, options:Partial<Options> = {}) {
this.$el = $el;
// Set default options, override with provided ones
this.options = {
repeat: true,
speed: 0.5,
...options
};
// Set options from eponymous data-* attributes
for (const option in this.options) {
if (this.$el.dataset[option] !== undefined) {
let value: any = this.$el.dataset[option];
// Cast numeric strings to numbers
value = isNaN(value) ? value : value;
// Cast 'true' and 'false' strings to booleans
value = (value === 'true') ? true : ((value === 'false') ? false : value)
// Attempt 1:
// ERROR: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Options'.
this.options[option] = value;
~~~~~~~~~~~~~~~~~~~~
// Attempt 2 (with assertions):
// ERROR (left-hand): Type 'string' is not assignable to type 'never'
// ERROR (right-hand): Type 'option' cannot be used as an index type.
this.options[option as keyof Options] = value as typeof this.options[option];
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~
}
}
/* ... */
}
/* ... */
}
Thanks!
CodePudding user response:
In your case their is not a lot of option. Since Options has keys with different types, you could do something like
const data = this.$el.dataset[option];
if (data !== undefined) {
if(option === 'repeat') {
const value: boolean = Boolean(data);
this.options.repeat = value;
...
}
...
}
Or use a switch case.
The problem is with the conversion from string to your type (number / boolean / ... )
CodePudding user response:
I can't speak to whether or not this is a best practice, but this approach seems sensible to me. I'd amend the Options
interface by another property genericOptions
:
interface Options {
repeat: boolean;
speed: number;
genericOptions: { [key: string]: string };
}
This property is any old type that can be indexed by a string to return a string.
And your constructor now looks like this:
constructor ($el:HTMLElement, options:Partial<Options> = {}) {
this.$el = $el;
// Set default options, override with provided ones
this.options = {
repeat: true,
speed: 0.5,
// Just an empty object by default, or you could make the `genericOptions`
// optional, depending on preference.
genericOptions: {},
...options
};
// Set options from eponymous data-* attributes
for (const option in this.options) {
if (this.$el.dataset[option] !== undefined) {
let value: any = this.$el.dataset[option];
// Cast numeric strings to numbers
value = isNaN(value) ? value : value;
// Cast 'true' and 'false' strings to booleans
value = (value === 'true') ? true : ((value === 'false') ? false : value)
// Set whatever generic options you need to.
this.options.genericOptions[option] = value;
}
}
/* ... */
}
CodePudding user response:
In a nutshell, just let TSC know that your Option type will have dynamic properties.
interface Options {
[key: string]: unknown;
repeat: boolean;
speed: number;
}
Then you can continue assigning the values dynamically
this.options[option] = value;
Your issue is quite similar to the one described here