Operator overloading with `symbol` keys

Use cases

The use/need for custom operators behaviour is somewhat inspired by C++'s ability to use custom operators on objects. This new system would use Symbol types to add well-known properties to objects and/or functions which would allow defining custom behaviour for objects, when operators are used on them.

One of dozens of potential use cases would be a Vector class. Consider the following example:

(I'm using TypeScript notation here for clarity)

class Vector {
  constructor(public x: number, public y: number) {
    // ...
  }

  add(vector: Vector): Vector {
    // Vector addition code
  }
}

let position = new Vector(10, 10);
const velocity = new Vector(1, 0);

position = position.add(velocity);

The above example calls the .add function to combine the values of two vector types, to produce another vector.

If the Vector class were instead defined like this

class Vector {
  // ...
  [Symbol.Plus](vector: Vector): Vector {
    return this.add(vector);
  }
}

then one could simply use the addition operator to perform the same action: position = position + velocity, therefore position += velocity. This would significantly improve code readability, as well as increasing the use-case for operators, as most operations are limited to primitive types.

Naturally, this would not have to be applicable to every object. For instance this would not make sense with Promise objects, but for scenarios, where custom behaviour can be used, this would allow the programmer to use syntax which more closely represents the algorithmic notation.

Potential points of friction.

Incompatible Object Types

If an object is operated upon by an object of different types, then undefined behaviour can occur. To mitigate this, it would be a TypeError if the second operand does not inherit from the first.

In the case where operator functions are defined on inline objects, any parameter is allowed, and simply relies on the operator function to determine the correct object type, throwing errors in the case of incompatible types.

Functions

Operators can be defined on functions using Object.defineProperty and the like, or by defining them on the function's this context, depending on how it was called.

Accidental (or deliberate) security holes

The following scenario shows how an attacker might use this functionality to produce a calling-side code execution system:


// Some library

function diffBetweenDates(date1: Date, date2: Date) {
  return date2 - date1;
}

// Attacker

const maliciousDate = {
  ...new Date(),

  [Symbol.Minus](date2: Date): Date {
    // Malicious code
  }
};

diffBetweenDates(harmlessDateObject, maliciousDate);

If instead the maliciousDate was created from a function with sensitive data in its context, the context of the operator's context may be linked to it, exposing it. I'm not certain how/if this would even become a serious issue, and what to do about it, if it were, however I think it noteworthy of mention here.

The current proposal has a section on why symbols aren't being used.

I'm not sure I understand the reasoning. Would it be possible to provide an example? Thanks

β€œThe meaning of operators on existing objects shouldn't be overridable or monkey-patchable, both for built-in types and for objects defined in other libraries.”

In other words you can’t mutate Array or a 3rd party class and directly modify it so it can then be used with certain operators.

The set of operators must be predictable to simplify the implementation for JS engines and also allow optimisations.

That makes sense, I guess.

Would it be possible to elaborate on the point of requiring a second property access?

They don't give a clear way to dispatch on the right operand, without requiring a second property access (like Python)

Thanks again

More information here:

  • Symbols don't explain how to dispatch on both operands. For example, if I add a vector type, it should be possible to multiply on the left with a scalar (number * vector ==> vector). What symbol method would I call on what to make it work? If we just dispatched on the left (like Smalltalk), we would have to monkey-patch Number to make it possible, which is undesirable.