skip to content

ToPrimitive, valueOf, and toString

Whenever an object meets an operator, the engine calls ToPrimitive with a hint. Understanding the hint and the valueOf/toString ordering lets you explain +, comparison, template interpolation, and Date arithmetic instead of guessing at the output.

part ofJavaScriptoverview, primer and where to startread it →
on this pageshow

questions

5

In JavaScript, why does '5' + 3 give '53' while '5' - 3 gives 2? Explain what the + operator does with mixed operand types.

level: juniorimportance: must knowfreq 70%

answer

  1. one operator, two meanings
  2. primitives first, then the string test
  3. either side being a string is enough
  4. no other arithmetic operator concatenates
  5. left to right, so strings spread

basics

~20 s

Binary + is the only arithmetic operator that is also string concatenation: after converting both operands to primitives, if either one is a string it concatenates, otherwise it adds numerically. Every other arithmetic operator converts to number unconditionally, so '5' - 3 is 2.

solid answer

~50 s

Binary `+` is overloaded. It first converts each operand to a primitive using the default hint, then checks the results: if **either** primitive is a string, both are converted to strings and concatenated; otherwise both go to `ToNumber` and are added. `'5' + 3` has a string operand, so it concatenates to `'53'`. Every other arithmetic operator — `-`, `*`, `/`, `%`, `**` — has no string meaning, so it calls `ToNumber` on both sides unconditionally and `'5' - 3` is `2`. This asymmetry is why `+` is the operator that surprises people: `1 + '2'` is `'12'` but `1 * '2'` is `2`, and `1 + 2 + '3'` is `'33'` while `'1' + 2 + 3` is `'123'`, because `+` is left-associative and the first string result poisons the rest of the chain.

code

javascript · 7 lines
javascript
console.log('5' + 3);      // '53'   concatenation
console.log('5' - 3);      // 2      subtraction coerces to number
console.log(1 + 2 + '3');  // '33'   left-associative
console.log('1' + 2 + 3);  // '123'
console.log(1 + null);     // 1      null → 0 on the number branch
console.log('1' + null);   // '1null' null → 'null' on the string branch
console.log(+'42' + 1);    // 43     unary + converts first

go deeper

for a junior

Be able to state the rule in one line — if either side is a string, + concatenates; every other arithmetic operator converts to number — and predict outputs for mixed literals without hesitating.

for a middle

Explain the ordering: primitives are produced first, then the string test decides the branch. Walk through a left-associative chain and show where the result flips to a string.

for a senior

Talk about the bug in production terms — a total that becomes a string because one value came from an input, a query parameter or JSON — and describe where you place explicit conversion and validation so it cannot recur.

for a principal

Frame the policy: numeric parsing belongs at system boundaries with validation, not scattered through business logic, and argue when a codebase should lean on a typed layer or schema validation instead of hand-rolled coercion.

## Two operators wearing one symbol In JavaScript the binary `+` token means either numeric addition or string concatenation, and which one you get is decided at runtime from the operand *values*, never from how the code looks. The specification's algorithm is short: 1. Evaluate both operands. 2. Convert each to a primitive via `ToPrimitive` with the **default** hint (objects go through `Symbol.toPrimitive`, otherwise `valueOf` then `toString`). 3. If **either** primitive is a string, apply `ToString` to both and concatenate. 4. Otherwise apply `ToNumber` (or `ToNumeric`) to both and add. Step 3 is the whole story: one string anywhere in the pair turns the operation into concatenation. Note the ordering — the primitive conversion happens *before* the string test, which is why an object that stringifies can flip a whole expression into concatenation. ## Why the other operators behave `-`, `*`, `/`, `%` and `**` have no second meaning. Their algorithm skips the string test entirely and converts both operands with `ToNumber`: ```js '5' - 3 // 2 '5' * '2' // 10 '10' / 2 // 5 '5' - 'a' // NaN — 'a' does not parse as a number ``` That is the fastest way to remember the rule: **only `+` can concatenate**. Some codebases even exploit it, using unary `+value` or `value * 1` as a terse numeric coercion. ## ToNumber on strings, briefly When a string is converted to a number the whole string must be a valid numeric literal after trimming whitespace, otherwise the result is `NaN`. The empty string is the trap: `Number('')` is `0`, not `NaN`, so `'' - 1` is `-1` and `[] * 2` is `0`. ```js Number(' 42 ') // 42 Number('42px') // NaN Number('') // 0 Number('\n') // 0 ``` ## Left-associativity turns one string into many `+` groups left to right, so a chain is evaluated pairwise and the first concatenation makes every later operand a string too: ```js 1 + 2 + '3' // '33' — (1+2) is 3, then 3 + '3' concatenates '1' + 2 + 3 // '123' — '1'+2 is '12', then '12'+3 1 + 2 + '3' + 4 // '334' ``` This is the mechanism behind the classic production bug where a numeric total silently becomes a string because one value arrived from an input field, `JSON` payload or `localStorage` as text, and every subsequent `total = total + item` appends instead of adding. ## Null, undefined and booleans Because these are primitives already, step 2 does nothing and step 4 applies `ToNumber`: `null` becomes `0`, `undefined` becomes `NaN`, `true` becomes `1`, `false` becomes `0`. ```js 1 + null // 1 1 + undefined // NaN 1 + true // 2 '1' + null // '1null' — string present, so ToString(null) is 'null' ``` Note how the same `null` becomes `0` in one line and `'null'` in the next: the operand's contribution depends entirely on whether the pair went down the string branch or the number branch. ## Objects on either side An object operand is converted before the branch is chosen, and with the default hint that means `valueOf` first, `toString` second for ordinary objects. Plain objects therefore produce `'[object Object]'` and arrays produce their comma-joined elements — both strings, so `+` concatenates: ```js [1, 2] + [3] // '1,23' ({}) + 'x' // '[object Object]x' [5] * 2 // 10 — '*' ignores strings, ToNumber('5') is 5 ``` ## Writing code that does not hit this The practical defences are boring and effective: convert explicitly at the boundary where untrusted text enters (`Number(input.value)` or `parseFloat`, then check `Number.isNaN`), keep numeric state numeric rather than re-parsing at each use, and prefer template literals or `String(x)` when you *want* concatenation so the intent is visible. If you see `+` used as "make this a number", remember unary `+` and binary `+` are different operators — unary `+` always converts to a number and never concatenates.

  • How would you safely add two values that might arrive as strings?
    Convert explicitly before the arithmetic and validate: `const n = Number(raw)` followed by a `Number.isNaN(n)` check, or `parseFloat` if trailing units are expected. Do the conversion once at the boundary where the text enters — a form field, JSON body or `localStorage` read — and keep the value numeric from then on, rather than re-coercing at every use site.
  • Is unary + the same operator as binary +?
    No. Unary `+x` is a separate operator that always performs `ToNumber` and can never concatenate, so `+'5'` is `5` and `+''` is `0`. Binary `+` is the overloaded one. That is why `+'5' + 3` is `8` while `'5' + 3` is `'53'` — the unary form converts before the binary form sees a string.

saying these in an interview costs you the question

  • Says + always concatenates when a string is involved, without mentioning other operators differ
  • Claims '5' - 3 is a syntax error or NaN
  • Thinks the left operand's type alone decides the outcome
  • Believes Number('') is NaN
  • Confuses unary + with binary + as one operator

context

open as a page

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?

level: middleimportance: must knowfreq 55%

basics

~20 s

ToPrimitive 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.

open as a page

In JavaScript, why does new Date() + 1 produce a string while new Date() - 1 produces a number?

level: middleimportance: should knowfreq 40%

basics

~20 s

Date is the one built-in whose Symbol.toPrimitive treats the "default" hint as "string". Binary + passes the default hint and gets the date's text form, so it concatenates; subtraction passes the "number" hint and gets the millisecond timestamp, so it subtracts.

open as a page

You are writing a Money class in JavaScript whose instances get logged, concatenated and compared. How do you take control of what those instances convert to, and what should the object do when it receives the "default" hint?

level: seniorimportance: should knowfreq 25%

basics

~20 s

Implement a Symbol.toPrimitive method taking the hint: return formatted text for "string", a number for "number", and for "default" either pick one deliberately or throw, so ambiguous uses like money1 + money2 fail loudly instead of silently concatenating.

open as a page

In JavaScript, ([] + {}) evaluates to the string '[object Object]', but a script line that starts with {} + [] evaluates to 0. Explain both results.

level: middleimportance: nice to knowfreq 30%

basics

~20 s

Two different mechanisms. In expression position both operands convert to primitives — '' and '[object Object]' — and + concatenates. At the start of a statement, {} parses as an empty block, so what remains is unary +[], which converts the empty array to '' and then to the number 0.

open as a page