Operator Overloading

GraalVM JavaScript provides an early implementation of the ECMAScript operator overloading proposal. This lets you overload the behavior of JavaScript’s operators on your JavaScript classes.

If you want to experiment with this feature, you will first need to enable it. Since both the proposal and our implementation of it are in early stages, you will need to set the following experimental option.

js --experimental-options --js.operator-overloading

After setting the option, you will see a new builtin in the global namespace, the Operators function. You can call this function, passing it a JavaScript object as an argument. The object should have a property for every operator you wish to overload, with the key being the name of the operator and the value being a function which implements it. The return value of the Operators function is a constructor that you can then subclass when defining your type. By subclassing this constructor, you get a class whose objects will all inherit the overloaded operator behavior that you defined in your argument to the Operators function.

Basic Example #

Let’s look at an example from the original proposal featuring vectors:

const VectorOps = Operators({
  "+"(a, b) {
    return new Vector(a.contents.map((elt, i) => elt + b.contents[i]));
  },
  "=="(a, b) {
    return a.contents.length === b.contents.length &&
           a.contents.every((elt, i) => elt == b.contents[i]);
  },
});

class Vector extends VectorOps {
  contents;
  constructor(contents) {
    super();
    this.contents = contents;
  }
}

Here we define overloads for two operators, + and ==. Calling the Operators function with the table of overloaded operators yields the VectorOps class. We then define our Vector class as a subclass of VectorOps.

If we create instances of Vector, we can observe that they follow our overloaded operator definitions:

> new Vector([1, 2, 3]) + new Vector([4, 5, 6]) == new Vector([5, 7, 9])
true

Example with Mixed Types #

It is also possible to overload operators between values of different types, allowing, for example, multiplication of vectors by numbers.

const VectorOps = Operators({
    "+"(a, b) {
        return new Vector(a.contents.map((elt, i) => elt + b.contents[i]));
    },
    "=="(a, b) {
        return a.contents.length === b.contents.length &&
            a.contents.every((elt, i) => elt == b.contents[i]);
    },
}, {
    left: Number,
    "*"(a, b) {
        return new Vector(b.contents.map(elt => elt * a));
    }
});

class Vector extends VectorOps {
    contents;
    constructor(contents) {
        super();
        this.contents = contents;
    }
}

To define mixed-type operators, we need to pass additional objects to the Operators function. These extra tables should each have either a left property or a right property, depending on whether we are overloading the behavior of operators with some other type on the left or on the right side of the operator. In our case, we are overloading the * operator for cases when there is a Number on the left and our type, Vector, on the right. Each extra table can have either a left property or a right property and then any number of operator overloads which will apply to that particular case.

Let’s see this in action:

> 2 * new Vector([1, 2, 3]) == new Vector([2, 4, 6])
true

Reference #

The function Operators(table, extraTables...) returns a class with overloaded operators. Users should define their own class which extends that class.

The table argument must be an object with one property for every overloaded operator. The property key must be the name of the operator. These are the names of operators which can be overloaded:

  • binary operators: "+", "-", "*", "/", "%", "**", "&", "^", "|", "<<", ">>", ">>>", "==", "<"
  • unary operators: "pos", "neg", "++", "--", "~"

The "pos" and "neg" operator names correspond to unary + and unary -, respectively. Overloading "++" works both for pre-increments ++x and post-increments x++, the same goes for "--". The overload for "==" is used both for equality x == y and inequality x != y tests. Similarly, the overload for "<" is used for all comparison operators (x < y, x <= y, x > y, x >= y) by swapping the arguments and/or negating the result.

The value assigned to an operator name must be a function of two arguments in the case of binary operators or a function of one argument in the case of unary operators.

The table argument can also have an open property. If so, the value of that property must be an array of operator names. These are the operators which future classes will be able to overload on this type (e.g. a Vector type might declare "*" to be open so that later a Matrix type might overload the operations Vector * Matrix and Matrix * Vector). If the open property is missing, all operators are considered to be open for future overloading with other types.

Following the first argument table are optional arguments extraTables. Each of these must also be an object. Each extra table must have either a left property or a right property, not both. The value of that property must be one of the following JavaScript constructors:

  • Number
  • BigInt
  • String
  • any class with overloaded operators (i.e. extended from a constructor returned by Operators)

The other properties of the extra table should be operator overloads as in the first table argument (operator name as key, function implementing the operator as value).

These extra tables define the behavior of operators when one of the operand types is of a type other than the one being defined. If the extra table has a left property, its operator definitions will apply to cases when the left operand is of the type named by the left property and the right operand is of the type whose operators are being defined. Similarly for the right property, if the extra table has a right property, the table’s operator definitions will apply when the right operand has the named type and the left operand has the type whose operators are being defined.

Note that you are free to overload any of the binary operators between your custom type and the JavaScript numeric types Number and BigInt. However, the only operators you are allowed to overload between your custom type and the String type are "==" and "<".

The Operators function will return a constructor that you will usually want to extend in your own class. Instances of that class will respect your overloaded operator definitions. Whenever you use an operator on an object with overloaded operators, the following happens:

1) Every operand that does not have overloaded operators is coerced to a primitive. 2) If there is an applicable overload for this pairing of operands, it is called. Otherwise, a TypeError is thrown.

Notably, your objects with overloaded operators will not be coerced to primitives when applying operators and you can get TypeErrors when applying undefined operators to them. There are two exceptions to this:

1) If you are using the + operator and one of the arguments is a string (or an object without overloaded operators that coerces to a string via ToPrimitive), then the result will be a concatenation of the ToString values of the two operands. 2) If you are using the == operator and there is no applicable overload found, the two operands are assumed to be different (x == y will return false and x != y will return true).

Differences from the Proposal #

There a few differences between the proposal (as defined by its specification and prototype implementation) and our implementation in GraalVM JavaScript:

  • You do not have to use the with operators from construction to enable the use of overloaded operators. When you overload operators for a class, those operators can then be used anywhere without using with operators from. Furthermore, our parser will not accept the with operators from clause as valid JavaScript.
  • You cannot use decorators to define overloaded operators. At the time of implementing this proposal, GraalVM JavaScript does not support decorators (these are still an in-progress proposal).
  • You cannot overload the "[]" and "[]=" operators for reading and writing integer-indexed elements. These two operators require more complex treatment and are not currently supported.