Type coercion

Beginner35 min

Predict what JavaScript does when an operator meets the wrong type, and convert values explicitly instead.

What you will learn
  • Predict the result of + and the other arithmetic operators on mixed types
  • Convert values on purpose with Number, String, Boolean and parseInt
  • List the falsy values and recognise truthy surprises
On this page

JavaScript does not refuse to add a number to a string or subtract text from a number. It converts one or both values and carries on. This is type coercion, and it explains most of the jokes about the language. Once you know the three rules, the results are predictable, and the habit of converting explicitly keeps them out of your programs. The fundamentals lesson on values and types covers type conversion in general.

The rules, in plain words

There are only three target conversions, and each one has a fixed table.

  • To a string (ToString): numbers become their digits, true becomes "true", null becomes "null", undefined becomes "undefined", an array becomes its items joined by commas, and a plain object becomes "[object Object]".
  • To a number (ToNumber): text is parsed as a whole, so "42" becomes 42, "" and whitespace become 0, and anything with extra characters, such as "12px", becomes NaN. true is 1, false and null are 0, and undefined is NaN.
  • To a boolean (ToBoolean): a short list of values is false and everything else is true. See the truthiness section below.

Objects first go through a step called ToPrimitive, which turns them into a primitive value by calling their valueOf or toString methods. Then one of the three conversions above applies.

The + operator

+ is the only arithmetic operator that also joins strings, and that makes it the usual source of surprises. The rule: if either side is a string (after ToPrimitive), both sides are converted to strings and joined. Otherwise both sides are converted to numbers and added.

The other arithmetic operators, -, *, /, % and **, always convert both sides to numbers.

plus_operator.mjs
console.log(1 + 2);
console.log("1" + 2);
console.log(1 + "2");
console.log(1 + 2 + "3");
console.log("1" + 2 + 3);
console.log("5" - 2);
console.log("5" * "2");
console.log("10" / "4");
console.log(true + 1);
console.log(null + 1);
console.log(undefined + 1);
console.log([] + []);
console.log([1, 2] + [3]);
console.log({} + []);
console.log("3" > "12", 3 > 12, "3" > 12);
Output
3
12
12
33
123
3
10
2.5
2
1
NaN
1,23
[object Object]
true false false

Work through a few of them:

  • 1 + 2 + "3" is "33": the sum 3 comes first, then the join. But "1" + 2 + 3 is "123": once a string appears, every later + joins.
  • "5" - 2 is 3, because - can only subtract numbers.
  • null + 1 is 1, since null becomes 0, and undefined + 1 is NaN, since undefined becomes NaN.
  • [] + [] is the empty string, and [1, 2] + [3] is "1,23", because each array becomes text first.
  • "3" > "12" is true, because two strings are compared character by character, like words in a dictionary. "3" > 12 is false because a number on one side makes it a numeric comparison.

Objects in the middle

An object that appears where a primitive is wanted is asked for one. For + and == the engine asks for a number-like value first, and for text contexts such as template literals and String(...) it asks for text. Plain objects and arrays answer with text: an array gives its items joined by commas, and a plain object gives "[object Object]". That is why {} + [] printed [object Object] above. Objects can define their own answers. The intermediate level covers how.

Converting on purpose

Explicit conversion has no surprises: you say what you want.

explicit_conversion.mjs
console.log(Number("42"), Number(" 7 "), Number(""), Number("12px"));
console.log(Number(true), Number(null), Number(undefined));
console.log(Number([]), Number([5]), Number([1, 2]));
console.log(String(123), String(null), String(undefined), String(true));
console.log(String([1, 2]), String({}));
console.log(Boolean(""), Boolean("0"), Boolean([]), Boolean(null));
console.log(+"8", `${5}`, 5 + "");
Output
42 7 0 NaN
1 0 NaN
0 5 NaN
123 null undefined true
1,2 [object Object]
false true true false
8 5 5
  • Number(value) is strict. Number("12px") is NaN, and Number("") is 0, which can be a trap for empty form fields. The unary plus, +"8", does the same thing but is less obvious to read.
  • String(value) works for anything, including null and undefined. value.toString() fails on those two.
  • Boolean(value) applies the truthiness table.
  • A template literal ${5}, or 5 + "", converts to a string too. The first is clearer.

parseInt and parseFloat

Number rejects text with leftovers. parseInt and parseFloat read as much as they can from the start of the text and stop at the first character that does not fit.

parse_int.mjs
console.log(parseInt("42px"), parseInt("px42"), parseInt("3.9"));
console.log(parseInt("101", 2), parseInt("ff", 16), parseInt("0x1f"));
console.log(parseInt("08", 10), parseInt("", 10));
console.log(parseFloat("3.14abc"), Number("3.14abc"));
Output
42 NaN 3
5 255 31
8 NaN
3.14 NaN
  • parseInt("42px") is 42, but parseInt("px42") is NaN, since nothing at the start matches.
  • parseInt("3.9") is 3: it stops at the dot. It truncates and does not round.
  • Always give a radix (the base) as the second argument: parseInt("101", 2) is 5. Without one, text beginning with 0x is read as hexadecimal. Writing parseInt(s, 10) for ordinary decimals removes any doubt.
  • parseFloat("3.14abc") is 3.14, while Number("3.14abc") is NaN.

One trap waits for later. If you pass parseInt straight to the array method map, it receives each item and its index, and the index is taken as the radix, so most results come out as NaN. The array methods lesson shows the fix.

Truthy and falsy

Conditions such as if (x) and operators such as ! and && convert their operand to a boolean. Exactly eight values are falsy: false, 0, -0, 0n, "", null, undefined and NaN. Everything else is truthy.

truthiness.mjs
console.log(Boolean(false), Boolean(0), Boolean(-0), Boolean(0n));
console.log(Boolean(""), Boolean(null), Boolean(undefined), Boolean(NaN));
console.log("--- truthy ---");
console.log(Boolean("0"), Boolean("false"), Boolean(" "));
console.log(Boolean([]), Boolean({}), Boolean(-1), Boolean(Infinity));
Output
false false false false
false false false false
--- truthy ---
true true true
true true true true

The surprises are on the truthy side: the text "0" and "false" are true, because any non-empty string is true. An empty array [] and an empty object {} are true as well, since they are objects. To test whether an array is empty, check list.length === 0.

Because 0 and "" are falsy, if (count) is false when count is 0, which is often a real value. The nullish lesson at the end of this module shows a tool for that.

!!value is a shorthand that converts to a boolean (two negations), and Boolean(value) says the same thing more clearly.

Common pitfalls

  • Adding numbers that arrived as text. Input from readline, forms and files is text. "2" + "2" is "22". Convert first with Number(...).
  • Number("") is 0. An empty answer becomes zero without a complaint. Check for empty text before converting.
  • parseInt without a radix, and passing parseInt straight to map.
  • Truthiness tests for numbers and text. if (value) skips 0 and "".
  • Relying on object-to-string conversion. "" + {} is "[object Object]", which is never what a user should see.
ExerciseCoercion quiz

Predict the output of these twelve lines, then run them. Expected result: twelve lines, which the solution lists.

console.log("2" + 2);
console.log("2" - 2);
console.log(2 + 2 + "2");
console.log("b" + "a" + +"a" + "a");
console.log(true + true);
console.log([] + 1);
console.log([2] * [3]);
console.log(null + "x");
console.log(undefined * 2);
console.log("" - 1);
console.log(!!"0");
console.log(1 < 2 < 3, 3 > 2 > 1);
Show solution
coercion_quiz.mjs
console.log("2" + 2);
console.log("2" - 2);
console.log(2 + 2 + "2");
console.log("b" + "a" + +"a" + "a");
console.log(true + true);
console.log([] + 1);
console.log([2] * [3]);
console.log(null + "x");
console.log(undefined * 2);
console.log("" - 1);
console.log(!!"0");
console.log(1 < 2 < 3, 3 > 2 > 1);
Output
22
0
42
baNaNa
2
1
6
nullx
NaN
-1
true
true false

In order: + joins because a side is text; - converts to numbers; the sum 4 is joined to "2"; +"a" is NaN, giving "baNaNa"; true is 1; [] becomes "", giving "1"; both arrays become numbers (2 and 3); null becomes "null"; undefined becomes NaN; "" is 0; the non-empty text "0" is truthy; and the last line compares true < 3 (true is 1) and then true > 1 (false).

ExerciseConvert user input safely

The four texts "10", "x", "" and "7.5" stand for things a user might type. For each one, print the text, Number(text) and parseInt(text, 10) on one line. Which conversions would you accept as valid numbers, and why is "" dangerous?

Input: none. Expected output: four lines, ending with 7.5 7.5 7.

Show solution

JSON.stringify prints the text with its quotes, so the empty string is visible:

safe_input.mjs
console.log(JSON.stringify("10"), Number("10"), parseInt("10", 10));
console.log(JSON.stringify("x"), Number("x"), parseInt("x", 10));
console.log(JSON.stringify(""), Number(""), parseInt("", 10));
console.log(JSON.stringify("7.5"), Number("7.5"), parseInt("7.5", 10));
Output
"10" 10 10
"x" NaN NaN
"" 0 NaN
"7.5" 7.5 7

Number("") is 0, so a blank answer looks like a real zero. parseInt gives NaN for it, which can be tested, but parseInt("7.5", 10) silently drops the .5. Check for blank text first, then use Number and test the result with Number.isNaN.

ExerciseAdd numbers that arrived as text

Declare const first = "12"; and const second = "30";. Print three values on one line: the two texts added with + as they are, the sum after converting each with Number(...), and the sum using the unary plus (+first + +second).

Input: none. Expected output: 1230 42 42.

Show solution
text_sum.mjs
const first = "12";
const second = "30";
console.log(first + second, Number(first) + Number(second), +first + +second);
Output
1230 42 42

With two strings, + joins them: "1230". Converting first makes it a real addition. The unary plus does the same conversion as Number, but Number(...) is easier to read.

Summary

  • + joins when either side is text and adds otherwise. -, * and / always use numbers.
  • Convert on purpose: Number(...), String(...), Boolean(...), and parseInt(text, 10).
  • Eight values are falsy: false, 0, -0, 0n, "", null, undefined, NaN. Empty arrays and objects are truthy.

Next: equality.