Search code examples
javascriptclassjavascript-objectsprototypeprototype-programming

Sub-class of Number persistently gets converted back to built-in Number Object/Function


In an effort not to pollute or extend the prototype Number object, I am attempting to create a class that extends Number. However, when I do this and then try to add anything to it or even add it to itself, it is converted back to a number.

I understand this is occurring because it is taking the numeric valueOf property value and returning that value added to another number, but I would like for the value to always retain its subclass without having to create additional methods like:

  • .add(x)
  • .sub(x)
  • .mul(x)
  • .div(x)

Is there a way to achieve this, similar to how both the Number and BigInt types allow for these types of computations? I could—but would prefer not to—extend the built-in Number object to support my custom methods, but anytime I try working with them now, they lose all their methods.

For example:

let num = 5;
console.log(num.constructor); // Number
num += 2;
console.log(num); // 7
console.log(num.constructor); // still Number

However, working with my extended class:

class MyNum extends Number {
    constructor (num = 0) {
        super(num);
    }
    get isEven() { return this.isInt && this % 2 === 0 }
    get isOdd() { return this.isInt && this % 2 !== 0 }
}
let num = new MyNum(5);
console.log(num.constructor); // MyNum
num += 2;
console.log(num); // 7
console.log(num.constructor); // back to Number

Furthermore, if the only solution here is to add methods like .add(x), would I need to recreate a new class instance whenever a computation like .add(x) is done, or is there another more native way to add to the existing object's value.

If the below is the only workaround, I may stick to extending the built-in Number object:

class MyNum extends Number {
    constructor (num = 0) {
        super(num);
    }
    add(x) { return new MyNum(this.valueOf() + x) }
    sub(x) { return new MyNum(this.valueOf() - x) }
    mul(x) { return new MyNum(this.valueOf() * x) }
    div(x) { return new MyNum(this.valueOf() / x) }
    mod(x) { return new MyNum(this.valueOf() % x) }
    get isEven() { return this.isInt && this % 2 === 0 }
    get isOdd() { return this.isInt && this % 2 !== 0 }
}

Solution

  • If you want

    num += 2;
    

    to result in num being a MyNum before, and a MyNum after, I'm afraid it isn't possible. As the specification says, all values will be converted to primitives (valueOf, toString), after which the primitives are either concatenated together or added together - and the concatenation or addition of primitives will result in plain primitives as the result, either a string, number, or BigInt.

    Adding new explicit methods like .add is the only real option.

    The value of a number object is in an internal slot which isn't changeable (not that you'd want to anyway, since mutation could be confusing). I don't think there's a better approach than add(x) { return new MyNum(this.valueOf() + x) } etc. But you can slightly clean up your methods by removing the valueOf calls, there's no need for them.

    add(x) { return new MyNum(this + x) }
    sub(x) { return new MyNum(this - x) }