Home > Mobile >  TypeScript: Dynamically set typed object properties
TypeScript: Dynamically set typed object properties

Time:08-09

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

  • Related