Skip to content
Advertisement

extending Number to get a flags type with instance methods

I would like to make a flag-enum and put instance methods on it. Since this is not directly possible with the enum construct, I decided to do the following experiment:

class SelectionsMade extends Number implements Number {
    constructor(value: any) {
        super(value);
    }

    static get None(): SelectionsMade { return new SelectionsMade(0); }
    static get StartDate(): SelectionsMade { return new SelectionsMade(1 << 0); }
    static get StartTime(): SelectionsMade { return new SelectionsMade(1 << 1); }
    static get EndDate(): SelectionsMade { return new SelectionsMade(1 << 2); }
    static get EndTime(): SelectionsMade { return new SelectionsMade(1 << 3); }

    static startDate(made: boolean): SelectionsMade { return made ? this.StartDate : this.None; }
    static startTime(made: boolean): SelectionsMade { return made ? this.StartTime : this.None; }
    static endDate(made: boolean): SelectionsMade { return made ? this.EndDate : this.None; }
    static endTime(made: boolean): SelectionsMade { return made ? this.EndTime : this.None; }

    inclStartDate(): boolean {
        return (this.valueOf() & SelectionsMade.StartDate.valueOf()) == SelectionsMade.StartDate.valueOf();
    }

    inclStartTime(): boolean {
        return (this.valueOf() & SelectionsMade.StartTime.valueOf()) == SelectionsMade.StartTime.valueOf();
    }

    inclEndDate(): boolean {
        return (this.valueOf() & SelectionsMade.EndDate.valueOf()) == SelectionsMade.EndDate.valueOf();
    }

    inclEndTime(): boolean {
        return (this.valueOf() & SelectionsMade.EndTime.valueOf()) == SelectionsMade.EndTime.valueOf();
    }

    removeStartDate(): SelectionsMade {
        return new SelectionsMade(this.valueOf() & ~SelectionsMade.StartDate.valueOf());
    }

    removeStartTime(): SelectionsMade {
        return new SelectionsMade(this.valueOf() & ~SelectionsMade.StartTime.valueOf());
    }

    removeEndDate(): SelectionsMade {
        return new SelectionsMade(this.valueOf() & ~SelectionsMade.EndDate.valueOf());
    }

    removeEndTime(): SelectionsMade {
        return new SelectionsMade(this.valueOf() & ~SelectionsMade.EndTime.valueOf());
    }
}

let s = new SelectionsMade(SelectionsMade.StartDate | SelectionsMade.EndDate);
console.log(s);

Yes, it’s a class that extends Number, has static getters to imitate the enum values, and some useful instance methods. And this does print 5 in TS playground, though you have to do intermediate cast to unknown so the type-checker is silenced.

I have 3 questions regarding this solution:

  1. how to avoid the caveat, that you need to call valueOf explicitly for TS to accept the class as a number (workaround being type assertion, also not nice)?
  2. [Opinions] Would you consider this a valid extension of Number, or would you rather go with static helper functions nevertheless?
  3. Is there a way to write modifying functions? e.g. I have the remove* methods that create a new value instead of modifying the value they are on, although the latter would be preferable.

Advertisement

Answer

how to avoid the caveat, that you need to call valueOf explicitly for TS to accept the class as a number…

You can’t. Your instances are objects, not primitive numbers. You need to use valueOf (or a unary + or similar) to get the underlying primitive number.

… (workaround being a type assertion, also not nice)?

Using a type assertion to treat an instance of SelectionsMade as a primitive number would be incorrect. It would just make TypeScript think it was a primitive number, when in fact it was still an object. That has implications for certain operations involving it (not least that typeof n would be "object" rather than "number").

Is there a way to write modifying functions? e.g. I have the remove* methods that create a new value instead of modifying the value they are on, although the latter would be preferable.

No. Number instances are immutable. There is no way to change the underlying primitive number the instance is wrapped around. You have to create a new instance.

User contributions licensed under: CC BY-SA
7 People found this is helpful
Advertisement