Learn JavaScript

Lesson 1 of 9 · Types, Operators and Coercion: Why '5' + 2 Is Not 7

Module 2 · Types, Operators and Coercion: Why '5' + 2 Is Not 7

Two Values Meet an Operator: The Picture

FreeReading

In this lesson

  • Predict what + does when a string sits on either side, and what -, * and / do with the same two values.
  • Explain what coercion is, and convert on purpose with Number(), String() and Boolean(), naming every falsy value.
  • Compare with === by default, say what == does differently, and test for NaN the one way that works.

Bob's first judged problem asks for the sum of two numbers. He pastes the starter, reads the two numbers, adds them and submits. The sample input is 5 2, and his program prints 52. The judge says Wrong Answer, and Bob says the judge is broken. The judge is fine. This lesson follows the engine, step by step, to the 52.

Bob's 52

Here is Bob's program. It is the fixed starter from Module 1, lesson 05, with three lines of his own where the comment was.

const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];

const a = next();
const b = next();
out.push(a + b);

console.log(out.join("\n"));
52

That output is for the input 5 2. The starter cuts the input into tokens, pieces of text between spaces. next() hands back the next token exactly as it was typed, so a is the string "5", not the number 5. Module 1 said it plainly: everything read from input is text until you convert it.

So a + b is "5" + "2", and + on two strings joins them. The fix is one word per line. nextInt() is Number(next()): it reads the token and converts it to a number.

const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];

const a = nextInt();
const b = nextInt();
out.push(a + b);

console.log(out.join("\n"));
7

So the same + gave 52 for two strings and 7 for two numbers. What an operator does depends on the types of its two values, and the rest of this lesson is the rule for each case.

What coercion is

An operator is a symbol that takes values and makes a new value: +, -, ===. The values it takes are its operands. Every operator expects operands of certain types. - subtracts numbers, so it wants two numbers.

Give - a string, and JavaScript does not stop with an error. It converts the string to a number first, then subtracts. A conversion the engine makes on its own, so that an operator can work, is called coercion (also "implicit conversion"). A conversion you write yourself, such as Number("5"), is an explicit conversion.

Watch the engine do it. The table below takes seven pairs of operands, one at a time, and shows each step the engine takes before the answer appears.

The engine converts one or both operands before the operator works. + joins text when either side is a string, and the other arithmetic operators turn both sides into numbers. == converts, and === does not.

ExpressionWhat the engine does firstResultType
"5" + 2a string is on one side, so 2 becomes "2""52"string
"5" - 2- wants numbers, so "5" becomes 53number
"5" * "2"both become numbers, 5 and 210number
5 + trueno string, so true becomes 16number
"a" * 1"a" is not a number, so it becomes NaNNaNnumber
1 == "1""1" becomes 1, then the two are comparedtrueboolean
1 === "1"nothing: different types are never strictly equalfalseboolean

Every row is real. Run them and you get exactly these values and types.

console.log("5" + 2, typeof ("5" + 2));
console.log("5" - 2, typeof ("5" - 2));
console.log("5" * "2", typeof ("5" * "2"));
console.log(5 + true, typeof (5 + true));
console.log("a" * 1, typeof ("a" * 1));
console.log(1 == "1", 1 === "1");
52 string
3 number
10 number
6 number
NaN number
true false

So coercion is not random. It is the engine following a rule for each operator, and the rules are short.

The + rule, and the rule for the others

+ has two jobs: it adds numbers and it joins strings. Joining text is called concatenation. Because of its two jobs, + has its own rule.

  • The + rule. If either operand is a string, the other one becomes a string too, and + joins them. Otherwise both become numbers, and + adds.
  • The rule for the others. -, *, /, % and ** have one job each, arithmetic. They always convert both operands to numbers.
The same two operands meet + and then - The same two values, two operators, two conversions operands conversion step result "5" string 2 number + one side is a string, so 2 becomes "2" "52" string "5" string 2 number - - only works on numbers, so "5" becomes 5 3 number The solid outline marks the operand that keeps its type. The dashed box is the coercion. + turned the number into text; - turned the text into a number.

Non-strings follow the number rule. true becomes 1 and false becomes 0. null becomes 0, and undefined becomes NaN.

console.log(10 - "4");
console.log("10" / "4");
console.log("2" ** 3);
console.log(true + 1);
console.log(null + 1);
console.log(undefined + 1);
console.log("Tk " + 250);
6
2.5
8
2
1
NaN
Tk 250

Only the last line had a string beside +, so only the last line joined text. So when you see +, look for a string on either side first. If there is none, it is addition.

Each operator family has its own lesson 02 section. Here they are on one card, so you know the whole set before you meet each one.

The four operator families

arithmetic   a + b   a - b   a * b   a / b   a % b   a ** b
comparison   a === b   a !== b   a < b   a > b   a <= b   a >= b   (and a == b, a != b)
logical      a && b   a || b   !a   a ?? b
assignment   x = v   x += v   x -= v   x *= v   x /= v   ...
  • Arithmetic makes a number from numbers. + alone also joins strings.
  • Comparison asks a yes-or-no question about two values and answers true or false.
  • Logical combines yes-or-no answers, and it uses the truthiness of each value, defined below.
  • Assignment stores a value in a variable; += and the rest are short for x = x + v and its relatives.

Converting on purpose: Number, String, Boolean

Coercion happens behind your back. A program you can trust converts on purpose, with three built-in converters. Each one takes a value of any type and gives back one type.

console.log(Number("42"), Number("3.5"), Number("12px"), Number(""));
console.log(String(42), String(true), String(null));
console.log(Boolean(1), Boolean(0), Boolean("hi"), Boolean(""));
42 3.5 NaN 0
42 true null
true false true false

Number(x) reads the whole string as a number, spaces at either end allowed. If any other character gets in the way, as in "12px", the answer is NaN. Note Number(""): an empty string becomes 0, not NaN. Lesson 03 shows the converters that read only the front of a string.

String(x) writes the value as text, the same text console.log prints. Boolean(x) answers one question: does this value count as true?

A value that counts as false is falsy. Every other value is truthy. The falsy list is short enough to learn by heart: six values, plus two other forms of zero.

Falsy valueWhat it is
falsethe boolean itself
0the number zero, and its two other forms: -0 and the bigint 0n
""the empty string, with no characters at all
null"no value, on purpose" (Module 1)
undefined"no value yet" (Module 1)
NaNthe failed number, explained next

Everything else is truthy, including some values that look empty. "0" and " " are strings with one character, so they are truthy. Lesson 03 prints the whole list, surprises included.

So Number, String and Boolean each say exactly which type you will get back. That is what makes them better than leaving it to the operator.

NaN, the number that is not a number

NaN stands for "Not a Number". It is what you get when a conversion to a number fails, or when arithmetic has no numeric answer, such as 0 / 0. Its type is still "number", because it is a special value of the number type.

NaN spreads. Any arithmetic with NaN in it gives NaN, so one bad value turns a whole sum into NaN. And it has one more odd property, which Zara found by testing it first.

const price = Number("abc");
console.log(price);
console.log(typeof price);
console.log(price === NaN);
console.log(NaN === NaN);
console.log(Number.isNaN(price));
NaN
number
false
false
true

NaN is not equal to anything, not even to itself. So x === NaN is false for every x, and it can never find a NaN. The test that works is Number.isNaN(x). So when a conversion might fail, check its result with Number.isNaN, never with ===.

=== checks the type and the value; == converts first

JavaScript has two equality operators. === is strict equality. It answers true only when both operands have the same type and the same value, and it never converts anything.

== is loose equality. When the types differ, it converts one or both operands first, as the widget showed for 1 == "1", and then compares. Its conversions follow a longer rule with some famous surprises.

console.log(7 === 7, 7 === "7");
console.log(7 == 7, 7 == "7");
console.log(0 == "", 0 === "");
console.log(null == undefined, null === undefined);
true false
true true
true false
true false

This track writes === and !== by default, and so do most teams. == has one place where it is the clearer choice, and lesson 04 shows exactly where. So compare with ===, and convert first on purpose if the types might differ.

Example 1: the type of every answer

The smallest program in the lesson. It prints each result beside its type, so the type change is visible.

console.log(4 + 4, typeof (4 + 4));
console.log("4" + 4, typeof ("4" + 4));
console.log("4" - 4, typeof ("4" - 4));
console.log("4" * "4", typeof ("4" * "4"));
console.log(4 / "four", typeof (4 / "four"));
8 number
44 string
0 number
16 number
NaN number

Only the second line has a string beside +, so only that answer is a string. The last line failed to convert "four" and still has the type number, because NaN is a number.

Run in Compiler
Example 2: Amara's PIN check

Amara's door lock stores its PIN as the number 4721. A guess arrives from the input, so it arrives as text. Her first check compares the token itself; her second converts it first.

const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];

const PIN = 4721;
const guess = next();
out.push("as typed:  " + (guess === PIN));
out.push("converted: " + (Number(guess) === PIN));

console.log(out.join("\n"));
as typed:  false
converted: true

That output is for the input 4721, the right PIN. The first check compares the string "4721" with the number 4721, and === says no, because the types differ. The second converts on purpose, and the same === says yes.

The parentheses around each comparison matter. Without them, + would join the label and the guess before === ran, and lesson 02 explains why.

Run in Compiler
Example 3: Zara skips the broken readings

Zara's weather station sends n readings. When a sensor fails, it sends err instead of a number. She converts every reading, skips the ones that come out as NaN, and averages the rest. The for loop and the if are the ones you met in Module 1, lesson 05; Module 4 teaches them properly.

const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];

const n = nextInt();
let sum = 0;
let good = 0;
for (let i = 0; i < n; i++) {
  const reading = nextInt();
  if (!Number.isNaN(reading)) {
    sum += reading;
    good += 1;
  }
}
out.push("good readings: " + good + " of " + n);
out.push("average: " + sum / good);

console.log(out.join("\n"));
good readings: 3 of 5
average: 21.333333333333332

That output is for the input 5, then 21.5 err 23 err 19.5. Without the Number.isNaN check, one err would make sum NaN, and the average would print NaN.

The last line shows the number type's long decimal. Lesson 03 rounds it for display with toFixed. Zara's first test was all err: then good is 0, and the average is 0 / 0, which is NaN.

Run in Compiler

Where this is used

  • Web forms. In every browser, an <input> element's value is a string, even when the field is type="number". A form total is Bob's 52 waiting to happen until the code converts it (Module 15).
  • Web addresses. URLSearchParams, built into browsers and Node, reads the query part of a URL. new URLSearchParams("page=2").get("page") returns the string "2", so page arithmetic needs Number() first.
  • Command-line tools. Node's process.argv holds the words typed after node script.js, and every one is a string. Running node add.js 5 2 hands the script "5" and "2".
  • JSON from a server. JSON.parse('{"price": 250}') gives a number, but '{"price": "250"}' gives a string. The sender decides the type, so code that reads JSON checks it (Module 8).

Common mistakes

1. Reading a number with next().

const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];

out.push(next() + next());

console.log(out.join("\n"));
52

No error and no warning: the program runs, prints 52 for the input 5 2, and the judge says Wrong Answer. Read every number with nextInt(). You will write next() because the token looks like a number on the screen, and the screen does not show types.

2. Comparing an input token with a number using ===.

const typed = "10";
console.log(typed === 10);
console.log(Number(typed) === 10);
false
true

The first line is false, silently, and the branch that depends on it never runs. Convert the token, then compare. You will make this mistake right after learning ===, because it is the right operator and the wrong type.

3. Looking for NaN with ===.

const value = Number("twelve");
console.log(value === NaN);
console.log(Number.isNaN(value));
false
true

value is NaN, and value === NaN still says false, because NaN equals nothing. Use Number.isNaN(value). You will write === NaN because it reads like plain English.

4. Trusting Number() to refuse an empty string.

const field = "";
console.log(Number(field));
console.log(Number.isNaN(Number(field)));
0
false

An empty field converts to 0, a real-looking number, and Number.isNaN lets it through. Check for "" before converting when an empty value means "missing". You will expect NaN because there is no number in the text, but the rule says an empty string is 0.

Brain teaser

Bob is sure these two lines print the same thing. Both use the same three values and the same operator.

console.log("3" + 4 + 5);
console.log(3 + 4 + "5");

What does each line print, and why are they different?

+ works on two operands at a time, from left to right. Apply the + rule to the first pair, then to its result and the third value.

Exercise 1Easy

Bob wants to see both of his answers side by side, the wrong one and the right one, so he never mixes them up again.

Input. Two tokens a and b, each a number written in decimal. A token may have a minus sign, a decimal point or leading zeros.

Output. Two lines. Line 1 is the sum of a and b as numbers, as JavaScript prints it. Line 2 is the two tokens joined as text.

Constraints. Each token has 1 to 25 characters.

Sample. Input 5 2 gives 7 and 52 on two lines.

const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];

// your code: read with next() and nextInt(), push every line of output to out

console.log(out.join("\n"));

Graded as sum-and-join. The hidden tests include leading zeros, negative numbers and decimals such as 0.1 0.2, so read each token once and keep it.

Run in Compiler
Exercise 2Easy

Zara gets a list of tokens from a sensor log and wants to know which ones are clean numbers. A token is clean when Number(token) is not NaN.

Input. n, then n tokens. No token contains a space.

Output. One line per token: yes, a space and the number as JavaScript prints it, when the token is clean; otherwise no.

Constraints. 1 <= n <= 30000. Each token has 1 to 20 characters.

Sample. Input 5 and 12 3.50 abc 007 12px gives yes 12, yes 3.5, no, yes 7 and no on five lines.

const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];

// your code: read with next() and nextInt(), push every line of output to out

console.log(out.join("\n"));

Graded as clean-numbers. The hidden tests try tokens that surprise people, such as NaN, Infinity and 0x1f. Predict each one before you run it.

Run in Compiler
Exercise 3Medium

Zara's weather station from Example 3 still fails one test: when every reading is err, the average prints NaN. Make the report say it in words.

Input. n, then n tokens. A token is a reading such as 21.5, or the word the sensor sends when it fails.

Output. Two lines. Line 1 is good: and the number of tokens that convert to a number. Line 2 is average: and their average as JavaScript prints it, or average: none when no token converts.

Constraints. 1 <= n <= 1000. Each token has 1 to 20 characters.

Sample. Input 4 and 20 err 22 err gives good: 2 and average: 21. Input 2 and err err gives good: 0 and average: none.

const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];

// your code: read with next() and nextInt(), push every line of output to out

console.log(out.join("\n"));

Not graded on its own. Start from Example 3 and test good before you divide. Then feed it the reading Infinity, and decide whether your station should count it.

Run in Compiler
Exercise 4Hard

Amara's door lock from Example 2 now keeps its PIN as text, 0472, because a PIN may start with 0. She sorts each guess three ways. Does it match the PIN as typed, match only once both are numbers, or not match at all?

Input. The PIN, then n, then n guesses. No token contains a space.

Output. One line per guess: text when the guess is the same text as the PIN, number when it is not but Number(guess) === Number(pin), and no otherwise.

Constraints. The PIN has 4 to 8 digits. 1 <= n <= 1000. Each guess has 1 to 20 characters.

Sample. Input 0472, 5 and 0472 472 +472 4.72e2 4720 gives text, number, number, number and no on five lines.

const input = require("fs").readFileSync(0, "utf8");
const tokens = input.split(/\s+/).filter(Boolean);
let at = 0;
const next = () => tokens[at++];
const nextInt = () => Number(next());
const out = [];

// your code: read with next() and nextInt(), push every line of output to out

console.log(out.join("\n"));

Not graded on its own. The hard part is seeing how many texts Number() reads as one number: a sign, a decimal point, an exponent. Which of the three answers should actually open a door?

Run in Compiler

Common doubts

  • Why does JavaScript convert at all? Python stops with an error.

    It does: in Python 3, "5" + 2 raises TypeError: can only concatenate str (not "int") to str. JavaScript was made in 1995 for small scripts inside web pages, and it was built to keep running rather than stop. That choice cannot be undone now without breaking old pages. So you convert on purpose, and the engine has nothing left to guess.

  • Why is typeof NaN "number"?

    Because NaN is a value of the number type, set aside to mean "this calculation has no numeric answer". It comes from IEEE 754, the standard for floating-point numbers that almost every processor follows, so C and Python have it too. Lesson 05 opens that standard up.

  • I have seen +x used to convert. Is that the same as Number(x)?

    Yes. A + with only one operand, written before a value, converts it to a number, exactly as Number(x) does. +"42" is 42. This track writes Number(x), because a reader cannot miss it.

  • Is coercion a bug in the language?

    No, it is a rule, and it is in the language standard, ECMAScript. Some of its results surprise people, and those are what interviews ask about (lesson 06). Lesson 04 shows where a conversion helps, and where it hides a bug.

Key takeaways

  • Coercion is the engine converting an operand's type so an operator can work; Number(), String() and Boolean() convert on purpose.
  • The + rule: a string on either side makes + join text. -, *, /, % and ** always convert to numbers.
  • Every token from input is a string: read numbers with nextInt().
  • The falsy values are false, 0 (with -0 and 0n), "", null, undefined and NaN; everything else is truthy.
  • NaN equals nothing, itself included, so test for it with Number.isNaN.
  • === compares type and value with no conversion, and it is the default; == converts first.
  • Go deeper: Under the Hood, the abstract equality algorithm and floating point (Pro).

Next, lesson 02 takes every operator one at a time, including the logical ones that return a value you might not expect.

End of lesson 1

Mark it done, and your progress moves with you.

Next: Every Operator, One by One