When JavaScript needs a primitive out of an object, it runs the ToPrimitive operation with a hint. What are the possible hints, and in what order does the engine try valueOf and toString for each?
answer
- objects are converted before operators run
- three hints, one preference each
- hint decides which method is tried first
- string hint flips the usual order
- valueOf returns this for plain objects
basics
~20 sToPrimitive takes a hint of "number", "string", or "default". For number and default it calls valueOf first, then toString; for string it calls toString first, then valueOf. The first call returning a primitive wins; otherwise a TypeError is thrown.
solid answer
~50 sAny operator that needs a primitive but gets an object runs ToPrimitive with one of three hints. `String(obj)` and template interpolation pass **string**; unary `+`, `-`, `*`, `<` and `Number(obj)` pass **number**; binary `+` and `==` pass **default**, which every built-in except `Date` treats exactly like number. If the object has a `Symbol.toPrimitive` method it is called with the hint and its result must be a primitive. Otherwise the engine runs the ordinary path: for number and default it tries `valueOf()` then `toString()`, for string it tries `toString()` then `valueOf()`, taking the first result that is a primitive. Plain objects inherit a `valueOf` that returns `this` — not a primitive — so they fall through to `toString()` and become `"[object Object]"`. If both steps return objects, you get `TypeError: Cannot convert object to primitive value`.
code
javascript · 11 linesconst box = {
value: 42,
valueOf() { return this.value; },
toString() { return `Box(${this.value})`; }
};
console.log(box * 2); // 84 — number hint: valueOf first
console.log(`${box}`); // 'Box(42)' — string hint: toString first
console.log(box + 1); // 43 — default hint behaves like number
console.log(String(box)); // 'Box(42)'
console.log(Number(box)); // 42go deeper
Recall the three hints and that String() prefers toString while arithmetic prefers valueOf. Be able to say why a plain object printed inside a string becomes "[object Object]".
Explain the full algorithm out loud: Symbol.toPrimitive first, otherwise valueOf/toString ordered by hint, first primitive wins, TypeError if neither yields one. Map concrete operators to the hint they pass.
Show you use this to diagnose real bugs — an id object landing in a URL as "[object Object]", a null-prototype config object throwing on string interpolation, a wrapper type comparing wrongly in a sort. Say how you make conversion explicit at the type's boundary.
Own the API-design angle: decide whether a domain type should be implicitly convertible at all, and argue for throwing on ambiguous conversion instead of silently producing a plausible-looking string that leaks into logs, cache keys and query parameters.
## The problem ToPrimitive solves JavaScript operators such as `+`, `-`, `<` and `==` are defined over primitive values — string, number, bigint, boolean, symbol, null, undefined. When you hand one of them an object, the specification does not fail; it first converts the object to a primitive through an internal operation called **ToPrimitive**. Almost every surprising coercion result comes from being able to predict what ToPrimitive returned. ToPrimitive takes two arguments: the value, and a *hint* — a string telling the object what kind of primitive the caller would prefer. There are exactly three hints. | Hint | Who passes it | |---|---| | `"string"` | `String(obj)`, template interpolation `` `${obj}` ``, property keys, `alert` | | `"number"` | `Number(obj)`, unary `+obj`, `-`, `*`, `/`, `%`, and relational `<` `>` `<=` `>=` | | `"default"` | binary `+`, and `==` when one side is an object | The hint is a *preference*, not a guarantee. `String({})` asks for a string and gets one, but `Number({})` asks for a number and gets `NaN` — because the object produced the string `"[object Object]"`, which `ToNumber` then failed to parse. ## The two-branch algorithm Step one: does the object have a `Symbol.toPrimitive` method (its own or inherited)? If so, the engine calls it with the hint as the single argument and uses the result. That result **must** be a primitive; returning an object from it throws a `TypeError`. ```js const temp = { celsius: 21, [Symbol.toPrimitive](hint) { return hint === 'string' ? `${this.celsius}°C` : this.celsius; } }; `${temp}` // '21°C' (hint 'string') temp * 2 // 42 (hint 'number') ``` Step two, if there is no such method: the engine runs **OrdinaryToPrimitive**, which tries two ordinary methods in an order chosen by the hint. - hint `"string"` → try `toString()`, then `valueOf()` - hint `"number"` or `"default"` → try `valueOf()`, then `toString()` Each candidate is called only if it is callable, and its result is accepted only if it is a primitive. Note the important detail: `"default"` behaves identically to `"number"` in this path. The distinction between them exists so that a type can *choose* to treat them differently — and in the standard library exactly one type does, `Date`. ## Why plain objects become "[object Object]" `Object.prototype.valueOf` is defined to return `this`. For a plain object that is the object itself — not a primitive — so the number/default path rejects it and falls through to `Object.prototype.toString`, which returns `"[object Object]"`. That is why `{} + ''`, `` `${{}}` `` and `String({})` all produce the same string. Arrays are the same story with a different `toString`: `Array.prototype.toString` calls `join(',')`, so `[1, 2].toString()` is `'1,2'` and `[].toString()` is `''`. That empty string is why `Number([])` is `0` and `[] + 1` is `'1'`. ```js String([1, 2]) // '1,2' Number([]) // 0 ('' → 0) Number([7]) // 7 ('7' → 7) Number([1, 2]) // NaN ('1,2' does not parse) ``` ## When conversion throws If both candidate methods are missing or both return objects, ToPrimitive throws `TypeError: Cannot convert object to primitive value`. The everyday way to hit this is an object with a null prototype, which inherits neither method: ```js const bare = Object.create(null); bare + ''; // TypeError: Cannot convert object to primitive value String(bare); // also throws JSON.stringify(bare); // '{}' — no conversion needed, works fine ``` The same happens if you deliberately write `{ valueOf: () => ({}), toString: () => ({}) }`. ## Reading real code with this The practical payoff is that you can evaluate coercions instead of memorising outcomes. `[] + {}` → both operands take the default hint → `''` and `'[object Object]'` → since one is a string, `+` concatenates → `'[object Object]'`. `[10] * 2` → number hint → `'10'` → `20`. `` `${[1,[2,3]]}` `` → string hint → nested `join` → `'1,2,3'`. And the design lesson: if a class of yours is ever concatenated, compared or logged, decide its conversion explicitly with `Symbol.toPrimitive` or a real `toString`, rather than letting it degrade into `"[object Object]"` inside a log line or a query string.
- If the hint is "number" but the object only has a toString, what do you get?The engine tries `valueOf` first, finds the inherited `Object.prototype.valueOf` which returns the object itself, rejects it as non-primitive, and falls through to your `toString`. The resulting string is then passed to `ToNumber`, so `{ toString: () => '7' } * 2` is `14`, while `{ toString: () => 'x' } * 2` is `NaN`. The hint never forces the method to return a number.
- Where does the "default" hint differ from "number" in practice?Only where a type distinguishes them, and in the standard library that means `Date`. `Date.prototype[Symbol.toPrimitive]` treats `"default"` like `"string"`, so `date + 1` concatenates while `date - 1` and `+date` use the timestamp. For every other built-in and for objects using the ordinary path, default and number produce identical results.
- Does JSON.stringify go through ToPrimitive?No. `JSON.stringify` has its own algorithm: it calls a `toJSON()` method if one exists, then serialises by type. `valueOf` is ignored, and an object with a null prototype serialises fine as `{}` even though `String()` on it throws. That is why an object can log as `"[object Object]"` yet stringify to useful JSON.
saying these in an interview costs you the question
- Says toString is always tried before valueOf
- Thinks the number hint forces the result to be a number
- Believes the hint is chosen by the object, not the operator
- Claims plain objects have no valueOf at all
- Assumes conversion can never throw