JAVASCRIPT / VALUES, TYPES, AND COERCION
Implicit coercion and why it surprises people
Predict what JavaScript does when an operator gets the wrong type, by tracing the operator's required type and each operand's trip through ToPrimitive.
What you will learn
- Decide what a mixed-type expression returns by looking at the operator, not the values
- Explain why "5" + 1 gives "51" but "5" - 1 gives 4
- Resolve object operands with Symbol.toPrimitive, valueOf, and toString by hint
- Catch coercion bugs in totals and comparisons built from string input
Understanding Implicit coercion and why it surprises people
Values never convert themselves; operators demand a type and convert whatever they are handed. Most arithmetic operators have exactly one meaning, arithmetic on numbers, so -, *, /, %, ** and unary + push both operands through numeric conversion and hold no surprises. Binary + is overloaded: it means both numeric addition and string concatenation, so it first reduces both operands to primitives and then inspects them, and if either primitive is a string it concatenates instead of adding. That single overload is behind most complaints that coercion is broken.
Objects add one step before any operator rule applies: the engine reduces the object to a primitive using an internal ToPrimitive operation with a hint of "number", "string", or "default". The hint does not fix the resulting type, it only chooses the order in which Symbol.toPrimitive, valueOf, and toString are consulted; number and default try valueOf first, string tries toString first. Binary + always passes "default", which is why [1, 2] + "" is "1,2" (an array's valueOf returns the array itself, so toString runs anyway) while a template literal passes "string" and can get a completely different text.
Relational operators follow a related but distinct rule: <, >, <=, and >= ask for primitives with the number hint, then compare code unit by code unit if both primitives turned out to be strings, and numerically otherwise. That is why "10" < "9" is true while "10" < 9 is false, with no error and no warning in between. Evaluation order matters too, because + is left-associative: one string anywhere in a chain infects everything to its right, so 1 + 2 + "3" is "33" but 1 + "2" + 3 is "123".
const qty = "3";
console.log(qty + 1, typeof (qty + 1)); // + sees a string
console.log(qty - 1, typeof (qty - 1)); // - has no string meaning
console.log(qty * "4"); // both sides become numbers
const box = { valueOf: () => 10, toString: () => "box" };
console.log(box + 1); // default hint: valueOf first
console.log(`${box}`); // string hint: toString first
console.log([1, 2] + [3]); // arrays have no useful valueOfCoercion is decided by the operator, not by the values, and binary + is the only arithmetic-looking operator that will settle for strings.
Worked examples
Comparison operators do not share the + rule
Shows that < compares strings by code unit but switches to numbers as soon as one side is a number.
const a = "10";
const b = "9";
console.log(a < b);
console.log(Number(a) < Number(b));
console.log(a < Number(b));
console.log("Z" < "a");Example explained
Line 1a < b leaves two strings, so "1" (code unit 49) is compared with "9" (57) and the answer is true.
Line 2Comparing the converted numbers gives 10 < 9, which is false, the answer most people expected.
Line 3a < Number(b) mixes types, so both sides go to numbers and the comparison becomes 10 < 9.
Line 4"Z" < "a" is true because code units are compared, not alphabetical or case-insensitive order.
An accumulator that silently becomes a string
Demonstrates how one string operand turns += into concatenation for the rest of the loop.
const fields = ["5", "10", "20"];
let broken = 0;
for (const v of fields) broken += v;
console.log(broken, typeof broken);
let fixed = 0;
for (const v of fields) fixed += Number(v);
console.log(fixed, typeof fixed);Example explained
Line 1broken += v expands to broken = broken + v, so the first iteration turns the number 0 into "05".
Line 2From then on the left operand is a string, so every later + concatenates and the 0 seed stays visible.
Line 3typeof broken proves the total was never numeric, even though nothing threw an error.
Line 4Converting each element before adding keeps + on its numeric branch and yields 35.
Controlling coercion with Symbol.toPrimitive
Shows the three hints an operator can pass and proves that + never asks for the string hint.
const price = {
amount: 25,
[Symbol.toPrimitive](hint) {
return hint === "string" ? "$25.00" : this.amount;
}
};
console.log(price + 5);
console.log(price * 2);
console.log(`${price}`);
console.log(price + "");Example explained
Line 1price + 5 passes the hint "default", so the numeric branch returns 25 and the sum is 30.
Line 2price * 2 passes "number" because multiplication has no string meaning at all.
Line 3The template literal passes "string" and therefore receives the formatted "$25.00".
Line 4price + "" still passes "default", giving "25" rather than "$25.00": the string operand does not change the hint.
Important notes
Unary + is a different operator from binary +: +"7" is the number 7, so 1 + +"7" is 8 while 1 + "7" is "17".
Date is the one built-in whose default hint behaves like the string hint, so d + 1 appends to a date string while d - 1 produces a timestamp number.
Common mistakes
Starting a total at 0 and adding form values: 0 + "5" + "10" is "0510", and calling toFixed on that result throws a TypeError far from the real cause.
Testing a suspected coercion bug with subtraction only: "5" - 1 is 4, so the code looks fine while the addition path still produces "51".
Comparing numeric strings with < or >: "10" < "9" is true, so a max-price or version check quietly selects the wrong item instead of failing.
Try it yourself
Change, predict, then run
In a browser console, write down your prediction and the expected typeof for 1 + "2" + 3, 1 + 2 + "3", and "6" * "7", then run them. Next create const obj = { valueOf: () => 5, toString: () => "five" } and predict obj + 1, obj * 2, and `${obj}` before checking.
Open the JavaScript workspaceCheck your understanding
Given const box = { valueOf: () => 2, toString: () => "10" };, what is the value of box + "1"?
- "101"
- 3
- "21"
- 21
Show answer
Binary + first asks each operand for a primitive using the default hint, and that hint consults valueOf before toString, so box becomes 2. Only afterwards does + look at the pair, and because the other operand is the string "1" it concatenates and returns the string "21". "101" assumes a string operand makes + request the string hint, but no operand can change which method is consulted; and since concatenation was chosen, the result is a string, not the number 21.