Type coercion
Understand the abstract operations behind JavaScript’s implicit conversions: ToPrimitive, ToNumber, ToString, ToBoolean, plus string concatenation and classic corner cases.
- 01Name each conversion stepSay when ToPrimitive, ToNumber, ToString, and ToBoolean run.
- 02Predict what + doesTell whether binary
+adds numbers or joins strings. - 03Explain famous puzzlesWork through puzzles like
[] == ![]one step at a time, without memorizing them.
Coercion is translation
Type coercion means JavaScript changes a value from one type to another on its own. It happens when an operator, a condition, or a built-in function needs a different kind of value than the one it got. For example, "6" - 1 needs two numbers, so JavaScript turns the text "6" into the number 6.
You met the everyday rules in the Type conversion lesson. This lesson shows the steps JavaScript follows inside, so you can explain each result yourself.
The rules come from the specification, or spec. The spec is the official rule book for JavaScript. It gives each conversion step a name. These named steps are called abstract operations. You can't call them from your code. But they run every time code like [] + {} or if (value) runs. This lesson uses four of them:
- ToPrimitive turns an object into a plain value.
- ToNumber turns a plain value into a number.
- ToString turns a plain value into text. In JavaScript, text is called a string.
- ToBoolean turns any value into
trueorfalse.
Each operator picks the conversion steps it needs. An object first becomes a plain value. Then the operator decides what it needs: a number, a string, or a simple true or false.
Think of each operator as a counter in an office. Before a counter can do its job, it asks for your value in the form it accepts. The * counter only takes numbers. The if counter only wants a yes or a no. The + counter asks two questions. First it asks both sides for a plain value. Then it checks whether either one is text.
- In real life: A clerk asks for one plain ID card, not your whole bag
- In JavaScript: ToPrimitive asks an object for one plain value
- In real life: A money desk changes your notes into the money the shop takes
- In JavaScript: ToNumber or ToString changes a plain value into a number or a string
- In real life: A door guard checks one short list of names
- In JavaScript: ToBoolean checks the short list of falsy values
The lesson uses small, real examples. It also has a tracer: a tool that shows each conversion step for a fixed list of expressions. A later lesson, A tour of the abstract operations, reads these same steps in the spec itself.
ToPrimitive comes first
STEP THROUGHA plain value is called a primitive. JavaScript has seven kinds of primitives:
- strings (text), such as
"hi" - numbers, such as
7 - booleans:
trueandfalse nullandundefined- symbols and bigints
Everything else is an object, such as an array or {}. An operator that needs a plain value has to ask the object for one. That request is called ToPrimitive.
The request comes with a hint. The hint is a word that says which kind of plain value the operation would like: "number", "string", or "default". The word "default" means "no preference".
For an ordinary object, JavaScript calls its valueOf() and toString() methods. The hint decides which one it tries first. An object can also have its own Symbol.toPrimitive method. Then JavaScript calls that method instead and passes it the hint. The Object to primitive conversion lesson covers these three methods in detail. Here we look at what happens after the object answers.
The code below gives an object its own Symbol.toPrimitive method, so you can see which hint each line sends. Step through it.
This object has its own Symbol.toPrimitive method, so you can watch ToPrimitive pick a plain value before each operator carries on.
script
const value = { [Symbol.toPrimitive](hint) { return hint === "number" ? 7 : "seven"; }}; console.log(String(value));console.log(value + "!");In plain words, line by line:
- Lines 1 to 5 make an object called
value. Its method returns7when the hint is"number", and"seven"for any other hint. - Line 7 uses unary +, a plus sign in front of one value. Unary
+always wants a number, so it sends"number"and gets7. - Line 8 calls
String(value). It sends"string"and gets"seven". - Line 9 uses binary +, a plus sign between two values. It does not add straight away. First it sends
"default"and gets"seven". That is a string, so+joins it with"!"and printsseven!.
+value asks with "number", String(value) with "string", and value + "!" with "default".An object is like a vending machine. You can't drink the machine. You press a button, and the machine drops out a drink. The operator presses a button too: the hint.
- In real life: The machine is not the drink
- In JavaScript: The object is not a plain value
- In real life: You press the water button or the soda button
- In JavaScript: The hint says which kind of plain value the operation wants
- In real life: The machine must drop out a drink, or show an error
- In JavaScript: ToPrimitive must give back a plain value, or throw an error
ToNumber & ToString
INTERACTIVEOnce the values are plain, most operators want either a number or a string.
- These use ToNumber: times
*, minus-, unary+, and comparisons such as<. - These use ToString: joining text with
+, putting a value inside a template literal, and showing a value as text.
const rawPrice = " 12 ";
const rawUnit = "12px";
console.log(Number(rawPrice));
console.log(Number(rawUnit));
console.log(String(12) + " items");In plain words, line by line:
- Lines 1 and 2 store two strings, the way they might arrive from a form.
- Line 4 prints
12.Number()removes the spaces around" 12 "and reads the number. - Line 5 prints
NaN, which means "not a number".Number()needs the whole text to be a number, andpxis not. It does not keep the12part. - Line 6 uses
String(12)on purpose to make the string"12". Then+joins" items", so it prints12 items.
The two tables below show more conversions. Read each row as: when you write this, you get this, for this reason.
| You write | You get | Why |
|---|---|---|
| Number(" 0x10 ") | 16 | The spaces are removed. 0x means the digits are hexadecimal (base 16). |
| Number("0b101") | 5 | 0b means the digits are binary (base 2). 101 in binary is 5. |
| Number("1e3") | 1000 | 1e3 means 1 times 10 to the power of 3. |
| Number("Infinity") | Infinity | JavaScript knows the word Infinity as a number. |
| Number("") | 0 | An empty string becomes 0. Many people find this surprising. |
| Number("12px") | NaN | The whole text must be a number. px is not, so the result is NaN. |
| Number(" ") | 0 | The spaces are removed. That leaves an empty string, which becomes 0. |
| Number(true) | 1 | true becomes 1, and false becomes 0. |
| Number(null) | 0 | null becomes 0. |
| Number(undefined) | NaN | undefined becomes NaN. |
| +1n | TypeError | Unary + runs ToNumber, and ToNumber refuses bigints. |
| Number(Symbol()) | TypeError | A symbol refuses to become a number. |
| You write | You get | Why |
|---|---|---|
| String(-0) | "0" | The minus sign is lost when -0 becomes a string. |
| String([1, 2]) | "1,2" | An array becomes its items, joined with commas. |
| String({}) | "[object Object]" | A plain object becomes the text [object Object]. |
| String(null) | "null" | null becomes the word null when you call String(). |
| String(undefined) | "undefined" | undefined becomes the word undefined when you call String(). |
| String(Symbol('id')) | "Symbol(id)" | Calling String() yourself works for a symbol. |
| "" + Symbol("id") | TypeError | Joining a symbol with + throws an error. |
The tracer below shows each conversion step for a fixed list of expressions. Pick an expression with a button. Read the code on the left. Then follow the steps on the right, down to the final value.
console.log([] + {});The array is asked for a plain value with the hint "default". valueOf() gives back the array itself, so toString() runs. It joins the items, and there are none.
""For a plain object, valueOf() also gives back the object itself, so toString() gives the plain value.
"[object Object]"At least one plain value is a string, so + joins strings instead of adding numbers.
"" + "[object Object]"Joining empty text adds nothing, so only the object's text is left.
"[object Object]"[] + {} gives "[object Object]". Both sides become plain values first. Then + sees a string and joins.
String(Symbol("id")) works, because you asked for a string yourself. This is an explicit conversion. But "" + Symbol("id") throws a TypeError. There JavaScript would have to convert the symbol on its own, an implicit conversion, and it refuses. This stops a symbol from turning into ordinary text by accident.
ToBoolean checks the short falsy list
A value is falsy if it counts as false in a condition, such as the one in an if. Every other value is truthy: it counts as true. ToBoolean is the simplest conversion step:
- It does not read text to find a number in it.
- It does not ask objects for a plain value.
- It only checks whether the value is on the short list of falsy values.
Here is the whole falsy list. Every value that is not in this table is truthy.
| Falsy value | What it is |
|---|---|
| false | The boolean false itself. |
| 0, -0, 0n | Zero, minus zero, and zero as a bigint. |
| "" | An empty string. Strings such as "0" and "false" are not empty, so they are truthy. |
| null | A value that is empty on purpose. |
| undefined | A value that was never set. |
| NaN | The number that means "not a number". |
| document.all | An old exception that only exists in browsers. Know it exists, but don't rely on it. |
The comments in the next block show each result. On line 4, Number("nope") gives NaN first, and NaN is falsy.
Boolean([]); // true: objects are truthy
Boolean("0"); // true: non-empty string
Boolean(0); // false: zero is falsy
Boolean(Number("nope")); // false: NaN is falsyconsole.log(Boolean("false"));
console.log(Boolean([]));
console.log(Boolean({}));
console.log(Boolean(NaN));In plain words, line by line:
- Line 1 prints
true."false"is a string that is not empty, so it is truthy. The word inside does not matter. - Lines 2 and 3 also print
true. An empty array and an empty object are still objects, and every object is truthy. ToBoolean does not count what is inside them. - Line 4 prints
false, becauseNaNis on the falsy list.
Boolean() does not read what is inside a value. "false" is text that is not empty. [] and {} are objects, and every object counts as true. Only NaN is on the short falsy list.To check whether an array has items, write items.length > 0. Don't use truthiness for that.
+ with strings: add or join?
STEP THROUGHBinary +, the plus sign between two values, causes the most famous coercion surprises. It can do two jobs: add numbers or join strings. It picks a job in four steps:
- Work out the value on the left, then the value on the right.
- If either value is an object, turn it into a plain value with ToPrimitive and the hint
"default". - If either plain value is a string, turn both into strings and join them.
- Otherwise, turn both into numbers and add them.
console.log("6" - 1);
console.log("6" * "2");
console.log("6" + 1);In plain words, line by line:
- Line 1 prints
5. Minus can only do math, so JavaScript turns"6"into the number 6. - Line 2 does the same for times and prints
12. - Line 3 is different. Binary
+can join strings, and"6"is a string. So JavaScript turns1into the string"1"and prints61.
So changing - to + can change both the job and the type of the result. The next example has more than one + in a row. Step through it and switch between the three expressions.
JavaScript works from left to right. Each + decides what to do using the two values it has at that moment.
script
console.log(result);Each + makes its own choice, one at a time, from left to right. That is why 1 + 2 + "3" gives "33", but "1" + 2 + 3 gives "123".
+ at a time. Each + looks only at its own two values. On the left, the first + sees two numbers and adds. On the right, text comes first, so every + joins text.true + 11 + 2null + 5[] + []"" + 0({ toString() { return "4"; } }) + 1
Sort each + expression by what the + does in the end: add numbers or join strings. Read the explanation even when your guess is right.
Corner cases, step by step
Coercion puzzles look random if you try to memorize the answers. They make sense once you name each conversion step. The table below shows famous puzzles. Read each row as: this code gives this result, and these are the steps. The tests for this lesson run every one of them.
| You write | You get | The steps |
|---|---|---|
| [] + [] | "" | Both arrays become empty strings. Then + joins two empty strings. |
| [] + {} | "[object Object]" | [] becomes "". {} becomes "[object Object]". Then + joins them. |
| ({}) + [] | "[object Object]" | The round brackets make {} an object. Without them, some consoles read {} at the start of a line as an empty block of code. |
| "b" + "a" + +"a" + "a" | "baNaNa" | +"a" gives NaN. Then + turns NaN into the string "NaN" and joins it with the other strings. |
| [] == ![] | true | ![] is false. Then == turns false into 0, and [] into "" and then 0. The next lesson covers ==. |
| undefined + 1 | NaN | undefined becomes NaN, and math with NaN always gives NaN. |
| 0.1 + 0.2 | 0.30000000000000004 | Nothing is converted here, because both are numbers. The tiny error comes from how numbers are stored (see the Numbers lesson). |
"". The empty object becomes "[object Object]". Now one side is text, so + joins the two.The next block makes two objects that look the same and compares them three ways.
const firstCart = { total: 5 };
const secondCart = { total: 5 };
console.log(firstCart === secondCart);
console.log(firstCart == secondCart);
console.log(firstCart == "[object Object]");In plain words, line by line:
- Lines 1 and 2 each make a new object. They look the same, but they are two separate objects.
- Lines 4 and 5 print
false. Comparing two objects checks whether they are the same object. It does not compare what is inside them. - Line 6 prints
true. Loose equality,==, turns the object into its usual string,"[object Object]", before it compares.
Line 6 is a puzzle to explain, not a style to copy. In real code, compare the fields you care about, or use strict equality, ===. The Equality & sameness lesson covers == in full.
The next block shows [] == ![] getting simpler, one step per line. It is a trace to read, not code to run.
[] == ![]
[] == false // ![] is false because arrays are truthy
[] == 0 // false becomes 0
"" == 0 // [] becomes "" through ToPrimitive
0 == 0 // "" becomes 0
trueInside an expression, ({}) + [] is the string "[object Object]". But some consoles read a whole line that starts with {} differently. If you type {} + [], the first {} may be read as an empty block of code, not an object. Only +[] is left, and that gives 0. Round brackets around {} make it an object again.
Use coercion on purpose in real code
Good code doesn't hide coercion. It makes every conversion easy to see. Convert data at the place where it comes into your program. Then the rest of your code works with the types it expects.
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;
}In plain words, line by line:
- A form field always gives you a string, even when the person typed digits.
- Lines 2 and 3 turn the two strings into numbers with
Number(). - Lines 5 to 7 stop early with a message if either one is
NaN.Number.isNaN()returnstrueonly forNaN. - Line 9 adds two real numbers, so
+does math.
function readQuantity(value) {
const quantity = Number(value);
return Number.isNaN(quantity) ? null : quantity;
}
console.log(readQuantity("3"));
console.log(readQuantity("3 items"));Line 6 prints 3. Line 7 prints null, because Number("3 items") is NaN.
Form fields, the query part of a web address, and JSON data from a server often give you strings. Convert them once, on purpose, with Number(), String(), or Boolean(). Then reject NaN before it reaches later math. Don't let + guess whether a checkout total is math or text.
Common misconceptions
“Coercion is random.”
It follows written rules in the spec. The results can surprise you, but the same four conversion steps explain them.
“ToBoolean uses valueOf or toString on objects.”
No. Ordinary objects are always truthy. ToBoolean never asks an object for a plain value.
“+ always turns both sides into numbers and adds.”
Binary + only adds when neither plain value is a string. If one side is a string, it joins strings.
“String(symbol) and "" + symbol are the same.”
String() can turn a symbol into a string. Joining a symbol with + throws a TypeError.
“0.1 + 0.2 is a coercion bug.”
Both values are already numbers, so nothing is converted. The tiny error comes from how computers store decimal numbers.
The four lines in the next table look alike, but each one runs a different conversion. Read each row as: this code runs this step and gives this result.
| You write | What runs | You get |
|---|---|---|
| Number("") | Number() turns the empty string into a number. | 0 |
| Boolean("") | ToBoolean finds the empty string on the falsy list. | false |
| "" + 0 | Binary + sees a string, so it joins. | "0" |
| 0 + "" | Binary + sees a string, so it joins. | "0" |
Practice: work out each step before you answer
5 EXERCISESWrite the three values the program prints, in order.
console.log([] + {});
console.log(+[]);
console.log("5" - [2]);[] + {} gives "[object Object]". +[] gives 0. "5" - [2] gives 3, because - turns both sides into numbers.
Use the step-by-step block in the corner cases section. What does [] == ![] give?
console.log([] == ![]);The last comparison is 0 == 0, so the answer is true. The next lesson explains == in full.
The function prints 205 now. Change it so the total is a number.
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, not "205".
Write a function for plain values. Then compare its results with Number() for the cases shown.
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 small version handles simple plain values. A string goes to Number(). true becomes 1 and false becomes 0. null becomes 0. undefined becomes NaN. Any other value makes it throw an error.
In one sentence, explain which conversion step makes +1n throw a TypeError.
Unary + always runs ToNumber. ToNumber refuses bigints, so +1n throws a TypeError. Keep 1n as a bigint. Write Number(1n) only when you really want a normal number.
Quiz: check your understanding
7 QUESTIONSQuestion 1 of 7Which conversion step 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 + 1print?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 line 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 changing a value's type because an operation needs a different kind of value.
- Objects usually go through ToPrimitive first. Then comes ToNumber or ToString.
- ToBoolean only checks the short falsy list. It never looks inside an object.
- Binary
+joins strings if either plain value is a string. Otherwise it adds numbers. - Puzzles are easier to explain when you name each conversion step than when you memorize the answers.
Remember the one-liner.
Type coercion is JavaScript running ToPrimitive, ToNumber, ToString, or ToBoolean because an operation needs a different kind of value.
Up next: Equality & sameness.