Symbol.toPrimitive

Controls the value an object produces under number, string and default coercion.

Signature
Symbol.toPrimitive
// as implemented on an object:
[Symbol.toPrimitive](hint)
  • Added in JavaScript ES2015

Parameters

ParameterTypeDescription
$hint"number" | "string" | "default"The kind of primitive the operation prefers: "number" for numeric coercion, "string" for string coercion, "default" when either is acceptable.

Return value

symbolThe well-known symbol used as the property key for an object's coercion method. That method must return a primitive value.

Errors

A TypeError is thrown if the method returns an object rather than a primitive.

Whenever an object must become a primitive — arithmetic, comparison, template interpolation, String(), Number() — the engine calls obj[Symbol.toPrimitive](hint) if present. This takes priority over both valueOf() and toString(), and one method handles every case.

Reading the hint

  • "number" — +obj, obj * 2, <, Number(obj)
  • "string" — String(obj), `${obj}`, use as a property key
  • "default" — obj + x, obj == x

When to reach for it

Reach for it on value types that have a natural number and a natural string form: money, durations, temperatures, vectors, wrapped dates. Implementing it makes such objects behave sensibly under + and < without callers reaching for a .toNumber() method.

What trips people up

The "default" hint is not a synonym for "number": + and == send "default", and forgetting to handle it makes concatenation return undefined. Always provide a fallback branch. The return value must be a primitive — returning an object throws a TypeError. Note that Date is the one builtin whose "default" behaves like "string", which is why date + "" gives text.

Example

Try it yourself
Loading editor…
Press Run to execute the code.

When to use it

  • Let a Money or Duration class take part in arithmetic and comparisons directly
  • Make a wrapper object interpolate into template literals with a formatted label
  • Give a range or version object a meaningful ordering under < and >

Frequently asked questions

What is the difference between Symbol.toPrimitive and valueOf?

Symbol.toPrimitive is consulted first and receives a hint telling it which primitive type is wanted, so one method covers every coercion. valueOf and toString are only used as a fallback when Symbol.toPrimitive is absent.

When is the hint 'default' used?

The default hint is passed by the + operator, by == comparisons, and by the Date constructor when given one object argument. Relational operators such as < use 'number' instead.

Can Symbol.toPrimitive return an object?

No. Returning a non-primitive throws a TypeError; the method must return a string, number, bigint, boolean, symbol, undefined or null.

More examples

Seeing which hint each operation sends

The log shows number, string, default, default, number in order — confirming that + and == use "default" while relational operators use "number". The last line shows Date is the builtin whose default hint behaves like a string.

Example · javascript
const probe = {
  [Symbol.toPrimitive](hint) {
    console.log("hint:", hint);
    return hint === "number" ? 1 : "one";
  },
};

+probe;
String(probe);
probe + 1;
probe == 1;
probe < 2;
console.log("---");
console.log(new Date(0) + "" !== String(Number(new Date(0))));

It wins over valueOf and toString

Both operations route through Symbol.toPrimitive and ignore valueOf and toString entirely. The second block shows the TypeError raised when the method returns an object instead of a primitive.

Example · javascript
const obj = {
  valueOf: () => 10,
  toString: () => "ten",
  [Symbol.toPrimitive]: () => 99,
};
console.log(obj + 1, `${obj}`);

const bad = { [Symbol.toPrimitive]: () => ({}) };
try {
  bad + 1;
} catch (err) {
  console.log(err.constructor.name);
}

Discussion

  • Be the first to comment on this lesson.