Type coercion
Understand the abstract operations behind JavaScript’s implicit conversions: ToPrimitive, ToNumber, ToString, ToBoolean, plus string concatenation and classic corner cases.
- 01Trace the translatorsExplain when ToPrimitive, ToNumber, ToString, and ToBoolean run.
- 02Predict + correctlyDecide whether binary + adds numbers or concatenates strings.
- 03Defuse corner casesWalk through puzzles like [] == ![] without memorizing them.
Coercion is translation
Type coercion is JavaScript converting a value from one type to another because an operator, condition, or built-in operation needs a different kind of value. You already met the practical rules in the Type conversion lesson. This lesson explains the internal translators behind those rules.
The most important translators are ToPrimitive, ToNumber, ToString, and ToBoolean. They are called abstract operations: names the specification uses to describe what the language must do. You do not call them directly, but you see their results every time code like [] + or if (value) runs.
Operators choose translators. Objects first become primitives. Then the operator decides whether it needs numbers, strings, or a simple truthy/falsy check.
Imagine each operator at a counter. Before it can work, it asks the value for the form it understands. The * counter wants numbers. The if counter wants a yes/no truthiness answer. The + counter is picky: first it asks both sides for primitives, then it checks whether either primitive is a string.
- In real life: A passport clerk asks for a plain ID before processing a request
- In JavaScript: ToPrimitive asks an object for a primitive value
- In real life: A currency desk turns bills into the currency the shop accepts
- In JavaScript: ToNumber or ToString translates primitives for an operator
- In real life: A bouncer checks one short yes/no list
- In JavaScript: ToBoolean checks the falsy list
Where the analogy stops: The engine is not literally calling human translators, and modern engines optimize aggressively. The analogy is about the specified result, not implementation details.
We will use small, real examples and a tracer for supported puzzles. The deeper spec tour later, A tour of the abstract operations, will revisit the formal algorithms in full.
ToPrimitive comes first
STEP THROUGHPrimitive values are strings, numbers, booleans, null, undefined, symbols, and bigints. Objects are not primitive, so an operator that needs a plain value must ask the object to produce one. That request is ToPrimitive.
For ordinary objects, JavaScript tries methods such as valueOf() and toString() in an order chosen by a hint: number, string, or default. The Object to primitive conversion lesson covers custom Symbol.toPrimitive, valueOf, and toString in detail; here we focus on what coercion does next.
A custom object lets us watch the first translator, ToPrimitive, choose a primitive before the operator continues.
script
const value = { [Symbol.toPrimitive](hint) { return hint === "number" ? 7 : "seven"; }}; console.log(String(value));console.log(value + "!");Notice line 9: binary + does not immediately add. It asks the object for a primitive with the default hint. Because the object returns a string, + concatenates.
An object is like a vending machine. The operator cannot drink the machine; it needs the machine to dispense something plain first. The hint is the button the operator presses.
- In real life: The machine is not the drink
- In JavaScript: The object is not the primitive
- In real life: Press water or soda
- In JavaScript: The hint asks for a preferred form
- In real life: The machine must dispense an item
- In JavaScript: ToPrimitive must return a primitive or throw
Where the analogy stops: Real vending machines can run out. JavaScript objects can compute, return different primitives, or throw; the analogy only shows the object-to-plain-value step.
ToNumber & ToString
INTERACTIVEOnce values are primitive, operators usually ask for a number or a string. Multiplication, subtraction, unary plus, and numeric comparison go toward ToNumber. String concatenation, template text, and many display operations go toward ToString.
| Expression | Result | Why |
|---|---|---|
| Number(" 0x10 ") | 16 | Whitespace is trimmed, and 0x reads as hexadecimal. |
| Number("0b101") | 5 | Binary string prefixes work in Number(). |
| Number("1e3") | 1000 | Exponent notation is numeric. |
| Number("Infinity") | Infinity | Infinity is a numeric string JavaScript recognizes. |
| Number("") | 0 | The empty string becomes 0, one of the classic surprises. |
| Number(true) | 1 | true becomes 1; false becomes 0. |
| Number(null) | 0 | null becomes 0. |
| Number(undefined) | NaN | undefined becomes NaN. |
| +1n | TypeError | Unary plus uses ToNumber, and ToNumber rejects BigInt. |
| Number(Symbol()) | TypeError | Symbols refuse numeric conversion. |
| Expression | Result | Why |
|---|---|---|
| String(-0) | "0" | The sign is not preserved when -0 becomes text. |
| String([1, 2]) | "1,2" | Arrays stringify by joining elements with commas. |
| String({}) | "[object Object]" | Plain objects inherit Object.prototype.toString. |
| String(Symbol('id')) | "Symbol(id)" | Explicit String() works for symbols. |
| "" + Symbol("id") | TypeError | Implicit string conversion through + throws for symbols. |
The next playground traces real expressions through a small set of supported shapes. Pick a preset, read the code, then compare the abstract-operation story with the final value.
console.log([] + {});Array uses the default hint. valueOf gives the array object, then toString joins its elements.
""Plain object valueOf is still an object, so toString supplies the primitive.
"[object Object]"At least one primitive is a string, so + concatenates strings instead of adding numbers.
"" + "[object Object]"The empty string disappears, leaving the object tag.
"[object Object]"[] + {} produces "[object Object]". Both operands become primitives before + decides to concatenate.
String(Symbol("id")) works because it is explicit. But implicit string conversion through "" + Symbol("id") throws. That strictness prevents symbols from silently becoming property names or display text where you did not ask for it.
ToBoolean checks the short falsy list
ToBoolean is refreshingly small. It does not parse strings and it does not ask objects for primitives. It checks whether the value is one of the falsy values; everything else is truthy.
| Falsy value | Notes |
|---|---|
| false | Already the boolean false. |
| 0, -0, 0n | Zero numbers and zero bigint. |
| "" | The empty string only. Strings like "0" and "false" are truthy. |
| null | Missing on purpose. |
| undefined | Missing by default. |
| NaN | Not a valid number. |
| document.all | A historic browser-only exception. Mention it, do not depend on it. |
Boolean([]); // true: objects are truthy
Boolean("0"); // true: non-empty string
Boolean(0); // false: zero is falsy
Boolean(Number("nope")); // false: NaN is falsyThis is why if (items) does not check whether an array is empty. Empty arrays are objects, and objects are truthy. Test the length when length is what you mean.
+ with strings: add or concatenate?
STEP THROUGHBinary + is the operator with the most famous coercion surprises. Its decision tree is:
- Evaluate the left expression, then the right expression.
- Run ToPrimitive on objects with the default hint.
- If either primitive is a string, convert both to strings and concatenate.
- Otherwise, convert both toward numeric values and add.
JavaScript evaluates the expression from left to right. Each + decides with the values it has at that moment.
script
console.log(result);The decision happens at each +, left to right. That is why 1 + 2 + "3" becomes "33", but "1" + 2 + 3 becomes "123".
true + 11 + 2null + 5[] + []"" + 0({ toString() { return "4"; } }) + 1
Sort each + expression by what the + operation ultimately does. Read the explanation even when your guess is right.
Corner cases without panic
Coercion puzzles look random when you memorize outputs. They become manageable when you name the translators. Here is a gallery of classic cases, all verified by tests.
| Expression | Result | Trace |
|---|---|---|
| [] + [] | "" | Both arrays ToPrimitive to empty strings; + concatenates. |
| [] + {} | "[object Object]" | [] becomes ""; {} becomes "[object Object]". |
| ({}) + [] | "[object Object]" | As an expression, the object is an object literal. In statement position, some consoles parse {} as a block before +[]. |
| "b" + "a" + +"a" + "a" | "baNaNa" | +"a" is NaN; string + turns NaN into "NaN". |
| [] == ![] | true | ![] is false; == then converts false to 0 and [] to "" then 0. Equality gets the next lesson. |
| undefined + 1 | NaN | undefined becomes NaN, and arithmetic with NaN stays NaN. |
| 0.1 + 0.2 | 0.30000000000000004 | This is floating-point precision from the Numbers lesson, not coercion. |
[] == ![]
[] == false // ![] is false because arrays are truthy
[] == 0 // false becomes 0
"" == 0 // [] becomes "" through ToPrimitive
0 == 0 // "" becomes 0
trueIn expression form, () + [] is the string "[object Object]". In a console where you type + [] as a whole statement, the leading braces may be parsed as an empty block, leaving +[], which is 0. Parentheses force the expression form.
Use coercion deliberately in real code
Professionals do not avoid coercion by pretending it does not exist. They make conversion points obvious. Convert at the boundary where data enters your program, then keep the rest of the code boring.
function checkoutTotal(form) {
const price = Number(form.price.value);
const shipping = Number(form.shipping.value);
if (Number.isNaN(price) || Number.isNaN(shipping)) {
return "Please enter valid numbers";
}
return price + shipping;
}Form fields, URL search parameters, and JSON from APIs often arrive as strings. Convert once with Number(), String(), or Boolean() when that is what you mean. Do not let + guess whether a checkout total is math or text.
Common misconceptions
“Coercion is random.”
It is specified. The results can be surprising, but the same abstract operations explain them.
“ToBoolean converts objects with valueOf or toString.”
No. Ordinary objects are always truthy. ToBoolean does not call ToPrimitive.
“+ always adds after Number().”
Only when neither primitive is a string. A single string makes binary + concatenate.
“String(symbol) and "" + symbol are the same.”
Explicit String() can describe a symbol. Implicit string conversion through + throws.
“0.1 + 0.2 is a coercion bug.”
Both values are already numbers. The result comes from binary floating-point precision, not type conversion.
| Code | Operation | Result |
|---|---|---|
| Number("") | Explicit ToNumber-like conversion | 0 |
| Boolean("") | ToBoolean | false |
| "" + 0 | Binary + with a string | "0" |
| 0 + "" | Binary + with a string | "0" |
Practice: trace before you guess
5 EXERCISESWrite the three values printed by the program, in order.
console.log([] + {});
console.log(+[]);
console.log("5" - [2]);[] + {} is "[object Object]". +[] is 0. "5" - [2] is 3 because - converts both sides to numbers.
Use the step list in the corner-case section. What does [] == ![] evaluate to?
console.log([] == ![]);The final comparison is 0 == 0, so the expression is true. The next lesson explains the loose equality algorithm fully.
The current function prints 205. Change it so the total is numeric.
function totalPrice(price, shipping) {
return price + shipping;
}
console.log(totalPrice("20", 5));function totalPrice(price, shipping) {
return Number(price) + Number(shipping);
}
console.log(totalPrice("20", 5));Number(price) turns "20" into 20 before + runs. Now + sees two numbers and prints 25 instead of "205".
Write a function for primitive inputs, then compare its results with Number() for the shown cases.
function toNumberLike(value) {
if (typeof value === "number") return value;
if (typeof value === "string") return Number(value);
if (typeof value === "boolean") return value ? 1 : 0;
if (value === null) return 0;
if (typeof value === "undefined") return NaN;
throw new TypeError("toNumberLike only accepts simple primitives");
}
console.log(toNumberLike(" 0b101 "));
console.log(toNumberLike(false));
console.log(Number.isNaN(toNumberLike(undefined)));function toNumberLike(value) {
if (typeof value === "number") return value;
if (typeof value === "string") return Number(value);
if (typeof value === "boolean") return value ? 1 : 0;
if (value === null) return 0;
if (typeof value === "undefined") return NaN;
throw new TypeError("toNumberLike only accepts simple primitives");
}
console.log(toNumberLike(" 0b101 "));
console.log(toNumberLike(false));
console.log(Number.isNaN(toNumberLike(undefined)));This tiny version handles simple primitives: strings delegate to Number(), booleans become 1 or 0, null becomes 0, and undefined becomes NaN. It throws for unsupported values.
In one sentence, explain the abstract operation behind the TypeError from +1n.
Unary plus is specified as numeric conversion through ToNumber. ToNumber rejects BigInt, so +1n throws a TypeError. Use 1n as a BigInt or Number(1n) explicitly only when you accept conversion to Number.
Quiz: check your understanding
7 QUESTIONSQuestion 1 of 7Which abstract operation usually runs first when an object meets +?
Choose an answer to see the explanation.
Question 2 of 7What does [1] + [2] print?
Read the code, then predictconsole.log([1] + [2]);Choose an answer to see the explanation.
Question 3 of 7What does null + 1 print?
Read the code, then predictconsole.log(null + 1);Choose an answer to see the explanation.
Question 4 of 7Which value is falsy?
Choose an answer to see the explanation.
Question 5 of 7What does the baNaNa expression print?
Read the code, then predictconsole.log("b" + "a" + +"a" + "a");Choose an answer to see the explanation.
Question 6 of 7What happens with +1n?
Choose an answer to see the explanation.
Question 7 of 7What does [] == ![] print?
Read the code, then predictconsole.log([] == ![]);Choose an answer to see the explanation.
Key takeaways
- Coercion is JavaScript translating values for the operation being performed.
- Objects usually go through ToPrimitive before ToNumber or ToString.
- ToBoolean is just the falsy list; it does not inspect object contents.
- Binary + concatenates if either primitive is a string; otherwise it adds numerically.
- Corner cases are easier to explain by naming the translator than by memorizing outputs.
Remember the one-liner.
Type coercion is JavaScript applying ToPrimitive, ToNumber, ToString, or ToBoolean because an operation needs a different kind of value.
Up next: Equality & sameness.