How Symbol.toPrimitive Works in JavaScript

Symbol.toPrimitive is a built-in JavaScript symbol that allows developers to customize how an object is converted into a primitive value (such as a string, number, or boolean). By defining a method with this symbol on an object, you override the default object-to-primitive coercion mechanisms, giving you complete control over implicit and explicit type conversions based on the context in which the object is used.

The Conversion Hints

When JavaScript needs to convert an object to a primitive, it passes a hint argument to the [Symbol.toPrimitive] method. There are three possible hint values:

  1. "number": Used when an operation explicitly or implicitly expects a numeric value. Examples include mathematical operators like subtraction (-), multiplication (*), bitwise operators, unary plus (+obj), or functions like Math.max().
  2. "string": Used when an operation expects a string. Examples include String(obj), template literals (`${obj}`), and object keys in property access.
  3. "default": Used when the engine is unsure what type to expect. The most common examples are the binary addition operator (+), which can perform either string concatenation or numeric addition, and the loose equality operator (==).

Implementation Example

You can implement Symbol.toPrimitive directly on an object or a class prototype. The method takes hint as a parameter and must return a primitive value.

const user = {
  name: "Alice",
  age: 30,

  [Symbol.toPrimitive](hint) {
    switch (hint) {
      case "string":
        return `User: ${this.name}`;
      case "number":
        return this.age;
      case "default":
        return `${this.name} (${this.age})`;
      default:
        throw new TypeError("Invalid hint");
    }
  }
};

// Coercion behaviors:
console.log(String(user)); // "User: Alice" (hint: "string")
console.log(+user);        // 30            (hint: "number")
console.log(user + 5);     // "Alice (30)5" (hint: "default")

Precedence Over valueOf and toString

In older JavaScript implementations, object-to-primitive conversion relied on defining separate valueOf() and toString() methods.

When [Symbol.toPrimitive] is defined on an object, it takes highest precedence. The JavaScript engine will execute [Symbol.toPrimitive] and completely bypass valueOf and toString. This provides a single, unified interface to handle all primitive conversion scenarios cleanly and predictably.