Learn JavaScript

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

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

Every Operator, One by One

FreeReading

In this lesson

  • Use every operator a working programmer writes, from + to ??=, and predict its result for numbers and strings.
  • Explain why &&, || and ?? hand back one of their operands, and set a default with || or ??.
  • Group an expression by precedence, and read the three TypeErrors an operator throws when it refuses a value.

Maria wants a message only when her score is exactly 5. Her variable holds 3, and she writes if (x = 5). The program runs with no error, and the message prints anyway. Nothing crashed and the answer is wrong, the worst kind of bug. The assignment section explains what she really asked.

An operator is a symbol that takes one, two or three values, its operands, and gives back a new value. This lesson visits every family once: its form, a few lines of code, their real output, and the one thing to watch. An operator costs nothing worth measuring, so the cost line of other topic lessons is left out on purpose.

Arithmetic: + - * / % **

Arithmetic

a + b   a - b   a * b   a / b   a % b   a ** b
+a   -a
  • % is the remainder after division, and ** raises a to the power b.
  • +a and -a take one operand: they turn it into a number, and -a also flips its sign.
console.log(7 + 2, 7 - 2, 7 * 2, 7 / 2);
console.log(7 % 2, -7 % 3, 2 ** 10);
console.log(+"42", -"3", +"", +"4 2");
console.log(10 - 2 - 3, 2 ** 3 ** 2);
9 5 14 3.5
1 -1 1024
42 -3 0 NaN
5 512

Division keeps the fraction (Module 1), and a remainder takes the sign of the left operand. A one-operand + converts exactly like lesson 01's Number(): "" becomes 0, and "4 2" becomes NaN.

Most operators group left to right, so 10 - 2 - 3 is (10 - 2) - 3. The power operator groups right to left: 2 ** 3 ** 2 is 2 ** 9, which is 512.

The one thing to watch: + joins text when either side is a string (lesson 01). So write Number(x) rather than +x when someone else will read the line. It does the same thing and says so.

Increment and decrement: ++ and --

Increment and decrement

x++   x--     postfix: gives the old value, then changes x
++x   --x     prefix: changes x, then gives the new value
  • x must be a let variable; every form adds or takes away 1.
let a = 5;
console.log(a++, a);
let b = 5;
console.log(++b, b);
5 6
6 6

Both variables end at 6. The difference is the value the expression itself gives: a++ handed console.log the old 5, while ++b handed it the new 6. On a line of its own, count++; and ++count; do exactly the same job.

The one thing to watch: use the value of x++ inside a bigger expression only when you mean the old value. 5++ stops before running with SyntaxError: Invalid left-hand side expression in postfix operation, because only a variable can change. On a const you get Module 1's TypeError: Assignment to constant variable.

Comparison: less, greater and the four equalities

Comparison

a < b     a > b     a <= b    a >= b
a === b   a !== b   a == b    a != b
  • Every comparison gives a boolean: true or false.
  • === and !== compare type and value with no conversion; == and != convert first.
console.log(3 < 5, 5 <= 5, 2 > 9, 9 >= 10);
console.log(1 === 1, 1 === "1", 1 !== "1");
console.log(1 == "1", 1 != "1", null == undefined);
console.log("10" < 9, "7" >= 7);
true true false false
true false true
true false true
false true

!== and != are the opposites of === and ==. Lesson 01 made === the default; lesson 04 shows where == earns its keep. A number against a string turns the string into a number, so "10" < 9 is 10 < 9, false.

The one thing to watch: when both sides are strings, nothing converts at all. That gives the surprise in the next section.

Two strings compare as text, not as numbers

const a = "10";
const b = "9";
console.log(a < b);
console.log(Number(a) < Number(b));
true
false

Every character has a number, its code: "1" is 49 and "9" is 57 (Module 3 shows the table). Two strings compare code by code from the left, the way a dictionary orders words. "10" and "9" differ at the first character, 49 against 57, so "10" comes first and the rest never matters.

The fix is to convert both sides on purpose, as the last line does; on the starter, read with nextInt(), not next(). So "10" < "9" is true, and Number("10") < Number("9") is false.

Logical operators return one of their operands

Logical operators

a && b   gives a if a is falsy, otherwise b
a || b   gives a if a is truthy, otherwise b
!a       gives true if a is falsy, otherwise false
  • Truthy and falsy are lesson 01's words: the six falsy values, and everything else truthy.
console.log(0 && "yes", 3 && "yes");
console.log("" || "guest", "Zara" || "guest");
console.log(!0, !"text", !!"text");
console.log(false && console.log("never printed"));
0 yes
guest Zara
true false true
false

The first two lines hold no true or false: && and || give back one of their operands, unchanged. Only ! always gives a boolean, so !!x means Boolean(x).

The last line shows short-circuit evaluation: when the left side decides the answer, the right side never runs, so the inner console.log was never called.

Put the two facts together, and name || "guest" sets a default: the name when it is truthy, "guest" otherwise. The one thing to watch: || replaces every falsy value, including some that might be real answers, such as a name left empty on purpose.

Nullish coalescing: ?? against ||

Nullish coalescing

a ?? b   gives a, unless a is null or undefined; then b
  • Nullish means null or undefined, Module 1's two "no value" values.
const nickname = "";
console.log("with ||: [" + (nickname || "guest") + "]");
console.log("with ??: [" + (nickname ?? "guest") + "]");
console.log(null ?? "guest", undefined ?? "guest", false ?? "guest");
with ||: [guest]
with ??: []
guest guest false

Zara left her nickname empty on purpose. || threw her choice away, because "" is falsy. ?? kept it, so the brackets on the second line are empty. ?? falls back only on null and undefined, and keeps every other value, false included.

The one thing to watch: ?? refuses to sit next to || or && without parentheses (Common mistake 4). Choosing between the two is lesson 04's job.

Optional chaining: ?. (a preview)

Optional chaining

value?.name   gives undefined if value is null or undefined, otherwise value.name
const user = { name: "Zara" };
const nobody = null;
console.log(user?.name, nobody?.name);
console.log(nobody?.name ?? "guest");
Zara undefined
guest

{ name: "Zara" } is an object, a value with named parts, and user.name reads one part. Module 8 teaches objects. On null, a plain dot stops the program: nobody.name throws TypeError: Cannot read properties of null (reading 'name'). ?. checks the left side first and gives undefined instead.

The one thing to watch: ?. turns a missing value into undefined, so pair it with ?? for a real default, as the last line does. Module 8 brings the rest.

The ternary: condition ? a : b

The conditional operator

condition ? valueIfTruthy : valueIfFalsy
  • condition is read as truthy or falsy; only one of the two values is worked out.
const marks = 72;
console.log(marks >= 40 ? "pass" : "fail");
const n = 7;
console.log(n % 2 === 0 ? "even" : "odd");
pass
odd

It is the only operator with three operands, hence the name ternary. Unlike an if (Module 4), it is an expression: it gives a value you can store, print or push to out.

The one thing to watch: one ternary reads well, and a ternary inside a ternary gets hard to read fast. Name a middle result with const, or add parentheses.

Assignment: = stores, it does not ask

Assignment

name = value   stores value in name, and the whole expression gives value

Here is Maria's program, run for real.

let x = 3;
if (x = 5) {
  console.log("x is five, and now x is", x);
}
x is five, and now x is 5

= is an operator too. It stores the right side in the name on the left, and the whole expression gives the stored value. So if (x = 5) stores 5 in x, then asks whether 5 is truthy. It is, so the message prints every time, and x has lost its 3.

Maria meant if (x === 5): === asks, = tells. Assignment groups right to left, so a = b = 0 sets b first, then a. The one thing to watch: this is valid JavaScript, so the engine never complains; a linter does (see "Where this is used").

Compound and logical assignment: += to ??=

Compound assignment

x += y   x -= y   x *= y   x /= y   x %= y   x **= y
x &&= y   x ||= y   x ??= y
  • x += y is short for x = x + y, and the same pattern holds for the other five arithmetic forms.
  • x ||= y assigns only when x is falsy, x &&= y only when it is truthy, x ??= y only when it is null or undefined.
let price = 200;
price -= 20;
price *= 2;
let title = "";
title ||= "Untitled";
let pages = null;
pages ??= 1;
console.log(price, title, pages);
360 Untitled 1

200 minus 20 is 180, and twice that is 360. title was empty, so ||= filled it; pages was null, so ??= filled it. The three logical forms joined the language in ES2021, and Node 22 runs them.

The one thing to watch: += follows the + rule, so on a string it joins text. let line = "a"; line += 1; leaves "a1".

The rest: comma, void, typeof, instanceof and the bitwise family

The others

(a, b)              comma: works out a, throws it away, gives b
void x              gives undefined
typeof x            gives the type's name as a string
x instanceof Type   asks whether x was made by Type
&  |  ^  ~  <<  >>  >>>   bitwise: named, not taught here
console.log((1, 2), void 0);
console.log(typeof 42, typeof "42", typeof null);
console.log([] instanceof Array, typeof []);
2 undefined
number string object
true object

You will meet the comma in a for header with two counters, and void 0 in code a tool generated; you never need to write either. typeof is Module 1's operator, so it needs no parentheses. instanceof says [] is an array (Module 7) where typeof only says "object"; Module 9 explains how.

The bitwise operators work on the 32-bit integer form of a number, for flags and hashes. MDN's "Expressions and operators" page lists them, and this track uses them only where a problem needs them. in, delete, new, ... and await arrive with their subjects in Modules 7, 8, 9 and 13.

Precedence: which operator goes first

When one expression holds several operators, precedence decides which runs first, like multiplication before addition in school maths. Associativity breaks a tie between operators on the same row: left to right for most, right to left for **, the ternary and assignment. Here is every operator this lesson taught, from first to last.

OrderOperatorsGroupsExample, grouped
1( )innermost first(1 + 2) * 3 is 9
2. ?. [ ] and a call f()left to rightuser?.name
3x++ x--one operanda++ * 2 is (a++) * 2
4! +x -x ++x --x typeof voidright to left!a && b is (!a) && b
5**right to left2 ** 3 ** 2 is 2 ** (3 ** 2)
6* / %left to right10 / 2 * 5 is 25
7+ -left to right1 + 2 * 3 is 7
8< > <= >= instanceofleft to right3 > 2 > 1 is (3 > 2) > 1
9=== !== == !=left to right1 + 1 === 2 is true
10&&left to righta || b && c is a || (b && c)
11|| ??left to right?? never mixes with || or && unless you add parentheses
12? : and every assignment, = to ??=right to lefta = b = 0 is a = (b = 0)
13,left to rightalways last

MDN's full table also places the bitwise operators. Now watch this one work on a single line.

How 1 + 2 * 3 > 6 && "yes" groups: four steps in precedence order One line, four steps: the highest row of the table goes first 1 + 2 * 3 > 6 && "yes" step 1: 2 * 3 is 6 step 2: 1 + 6 is 7 step 3: 7 > 6 is true step 4: true && "yes" is "yes" Rows 6, 7, 8 and 10 of the table, in that order: * before +, both before >, all before &&. With parentheses: ((1 + (2 * 3)) > 6) && "yes"

Run the line yourself and you get the same answer the steps give.

console.log(1 + 2 * 3 > 6 && "yes");
console.log(((1 + (2 * 3)) > 6) && "yes");
yes
yes

For anything you are not sure of, the rule is simple: when in doubt, add parentheses. They cost nothing, and the next reader does not have to open this table.

When an operator refuses: three TypeErrors

Most operators convert whatever they are given. Three conversions are refused, and the program stops with a TypeError. The Playground's verdict is Runtime error, and the lines printed before the stop stay.

1. A bigint mixed with a number. Module 1 named the n suffix for whole numbers past the safe limit.

const big = 10n;
console.log(big + 5n);
console.log(big + 5);
15n
TypeError: Cannot mix BigInt and other types, use explicit conversions

A bigint never turns into a number silently, or back, because either way could lose digits. Convert on purpose: big + BigInt(5) gives 15n. Comparing is allowed: 10n > 5 is true.

2. A symbol joined to text. A symbol is the rare primitive from Module 1, a unique label.

const id = Symbol("id");
console.log(typeof id);
console.log("id: " + id);
symbol
TypeError: Cannot convert a Symbol value to a string

+ would turn the symbol into text without asking, and a symbol refuses that. Ask for it out loud instead: String(id) gives "Symbol(id)".

3. An object with no way to become a primitive. A primitive is one of Module 1's seven simple types. Node's built-in querystring module turns the text after a ? in a web address into an object.

const qs = require("querystring");
const form = qs.parse("name=Zara&age=14");
console.log(form.name, form.age);
console.log("form: " + form);
Zara 14
TypeError: Cannot convert object to primitive value

To join an object to text, the engine asks the object for its text form, using methods most objects inherit (Module 9). This object inherits none, so there is nothing to ask. Print its parts, or pass it as its own argument: console.log("form:", form) prints form: [Object: null prototype] { name: 'Zara', age: '14' }.

So an operator converts silently except in these three cases, and each fix is to convert on purpose, lesson 03's subject.

Example 1: splitting a bill

Three friends share a bill of 1000 taka. Each pays a whole number of taka, and the coins left over go in the tip jar.

const bill = 1000;
const friends = 3;
const share = Math.floor(bill / friends);
const left = bill % friends;
console.log("each pays", share);
console.log("left over", left);
console.log("check:", share * friends + left === bill);
each pays 333
left over 1
check: true

Math.floor rounds down, as Module 1's teaser showed. The last line needs no parentheses: * runs first, then +, then ===, rows 6, 7 and 9 of the table.

Run in Compiler
Example 2: David's game, one quest later

David's game keeps a player's points, level, streak and badge. One finished quest touches all four, each with a different operator.

let xp = 180;
let level = 3;
let streak = 0;
let badge = "";
xp += 45;
streak++;
level += xp >= 200 ? 1 : 0;
badge ||= "first steps";
console.log("xp", xp, "level", level, "streak", streak);
console.log("badge:", badge);
console.log(level > 3 && streak >= 1 ? "level up!" : "keep going");
xp 225 level 4 streak 1
badge: first steps
level up!

In level += xp >= 200 ? 1 : 0, the comparison runs first, then the ternary picks 1, then += adds it. The last line works the same way: both comparisons, then &&, then the ternary. Change xp to 100 and run it again: the level stays 3 and the message changes.

Run in Compiler
Example 3: Zara's checkout

Zara's shop reads the number of items, their prices, then an optional coupon word. Orders of 1000 taka or more get 10% off. Delivery costs 60, unless the order is 500 or more after the discount or the coupon is FREESHIP.

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 total = 0;
for (let i = 0; i < n; i++) {
  total += nextInt();
}
const coupon = next() ?? "none";
const discount = total >= 1000 ? total / 10 : 0;
const delivery = total - discount >= 500 || coupon === "FREESHIP" ? 0 : 60;
out.push("total: " + total);
out.push("discount: " + discount);
out.push("coupon: " + coupon);
out.push("delivery: " + delivery);
out.push("to pay: " + (total - discount + delivery));

console.log(out.join("\n"));
total: 1150
discount: 115
coupon: none
delivery: 0
to pay: 1035

That output is for the input 3 450 300 400, with no coupon. The for loop is Module 1's (lesson 05); Module 4 teaches it. When the input runs out, next() gives undefined, and ?? turns that into "none".

Read the delivery line with the table: subtraction, both comparisons, ||, then the ternary. The input 2 200 150 FREESHIP gives delivery: 0 and to pay: 350. Without the parentheses on the last push, + would join the text first, and the subtraction would give NaN.

Run in Compiler

Where this is used

  • Next.js configuration. The Next.js documentation's assetPrefix page sets isDev with phase === PHASE_DEVELOPMENT_SERVER, then writes assetPrefix: isDev ? undefined : 'https://cdn.mydomain.com'. That is a strict equality and a ternary choosing a setting.
  • TypeScript. When told to produce older JavaScript (target ES2019), TypeScript 5.9 rewrites config?.server?.port ?? 3000 into === null, === void 0 and ? : tests. That is where you will meet void 0, and it shows exactly what ?. and ?? check.
  • ESLint. Its no-cond-assign rule, on in the recommended set, reports an assignment inside an if condition. It would have flagged Maria's if (x = 5) before she ran it.

Common mistakes

1. A range test written as in maths.

const temp = 30;
console.log(10 < temp < 20);
console.log(10 < temp && temp < 20);
true
false

No message, and the first answer is wrong: 30 is not between 10 and 20. 10 < temp gives true, then true < 20 turns true into 1, and 1 is less than 20. Join two comparisons with &&, as the second line does. You will write the first form because maths does.

2. Comparing two tokens read 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 = [];

const oldPrice = next();
const newPrice = next();
out.push(newPrice > oldPrice ? "went up" : "did not go up");

console.log(out.join("\n"));
did not go up

For the input 90 150, Bob's program says the price did not go up. Both tokens are strings, so "150" > "90" compares "1" with "9": false. Read with nextInt() and it says went up. You will use next() because the tokens look like numbers.

3. A minus sign before a power.

console.log(-2 ** 2);

The program stops before any line runs: SyntaxError: Unary operator used immediately before exponentiation expression. Parenthesis must be used to disambiguate operator precedence. It could mean 4 or -4, so JavaScript makes you choose: (-2) ** 2 is 4, and -(2 ** 2) is -4. You will write it bare because it looks like maths.

4. Mixing ?? with || in one expression.

const typed = "";
const saved = null;
const name = typed || saved ?? "guest";

The program stops before any line runs: SyntaxError: Unexpected token '??'. The language refuses to guess which of the two you meant to run first. Add the parentheses you mean: (typed || saved) ?? "guest". You will write it because both operators set defaults, and they look like one family.

Brain teaser

Kenji stores a volume setting that the player turned all the way down, to 0. Then he reads it back two ways.

console.log(0 || "default");
console.log(0 ?? "default");

Predict both lines before you run them. Which line keeps the player's choice, and which one throws it away?

Is 0 truthy or falsy? Is 0 null or undefined? Each operator asks only one of those two questions.

Exercise 1Easy

Amara never runs an expression before she has guessed its result. Write your prediction for each of the ten lines below: the value, and its type. Then run the program and mark your guesses.

Input. None.

Output. Ten lines, one per expression. For each wrong guess, find the section that explains it.

// Write your ten predictions first, then run.
console.log(7 % 3 + 2 ** 3);
console.log("8" * "2");
console.log("25" < "3");
console.log(10 / 4 > 2 && "big");
console.log(null || 0 || "last");
console.log(false ?? "off");
console.log(!!"false");
console.log(1 + 2 === 3 ? "ok" : "odd");
console.log(typeof (4 > 3));
console.log(2 ** 3 ** 2);

Not graded on its own. The point is the guess before the run.

Run in Compiler
Exercise 2Easy

Alice judges a cooking contest with three judges per dish, and each dish keeps its best score. Print the largest of each three numbers. Use the ternary operator only: no if, and no Math.max.

Input. A line with t, then t lines of three numbers each.

Output. t lines: the largest of each three, as JavaScript prints it.

Constraints. 1 <= t <= 10000. Each number has 1 to 20 characters.

Sample. Input 3, then 3 9 4, -1.5 -7 -2 and 5 5 5, gives 9, -1.5 and 5 on three 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 largest-of-three. Try equal numbers, negative numbers and decimals before you submit, and read with nextInt(): tokens compared as strings give wrong answers.

Run in Compiler
Exercise 3Medium

Maria's sign-up form sends each field as one line. A missing field arrives as the word null or undefined; a field left empty on purpose arrives as an empty line, and stays empty. Print each field in brackets, with default for a missing one.

Input. A line with n, then n lines. A line that is exactly null means null, a line that is exactly undefined means undefined, and any other line, an empty one included, is that text.

Output. n lines: for each field, [, then the value or default when the value is null or undefined, then ].

Constraints. 1 <= n <= 10000. Each field line has 0 to 60 characters, and the last one is never empty.

Sample. Input 4, then the lines Zara, null, an empty line and undefined, gives [Zara], [default], [] and [default] on four lines.

A line may be empty, so the starter is the lines variant: the two token lines become const lines = input.split("\n"); and a next() that returns one whole line.

const input = require("fs").readFileSync(0, "utf8");
const lines = input.split("\n");
let at = 0;
const next = () => lines[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 given-or-default. Try an empty line, a line that reads 0 and a line that reads NULL in capitals, which is text, not null.

Run in Compiler
Exercise 4Hard

Kenji says he never needs parentheses, because he knows the table. Here are six lines he wrote. Under each out.push, add a second one with the same expression and every group in parentheses, exactly the way the engine groups it. Run it: each pair of lines must print the same value.

const out = [];
const a = 0;
const b = "";
const c = "c";

out.push(1 + 2 * 3 ** 2);
out.push(10 - 4 - 3 ** 0);
out.push(a || b && c);
out.push(!a == b);
out.push(typeof 1 + 1);
out.push("a" + 1 + 2 * 3);

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

Not graded on its own. Two of the six surprise most people. Explain each of those two in one sentence that names its row of the precedence table.

Run in Compiler

Common doubts

  • Do I need to learn the precedence table by heart?

    No. Know * before +, comparisons before && and ||, and assignment last. Put parentheses around anything else. Code that needs the table to be read is code that needs parentheses.

  • Why do && and || not just return true or false?

    Returning an operand makes a default one short expression, name || "guest". Inside an if or a ternary, nothing breaks, because the result is read as truthy or falsy anyway. When you really need a boolean, write Boolean(x) or !!x.

  • Why is typeof an operator and not a function?

    An operator can look at a name before the engine looks it up. That is why typeof on a name that was never declared gives "undefined" instead of throwing (Module 1, lesson 04). typeof(x) also works, because the parentheses only group.

Key takeaways

  • Arithmetic turns its operands into numbers, except + with a string on either side; % keeps the left operand's sign and ** groups right to left.
  • x++ gives the old value and ++x the new one; alone on a line, they do the same job.
  • && and || return an operand, so || sets a default; ?? sets one only for null and undefined.
  • Two strings compare as text, so "10" < "9"; convert with Number() or read with nextInt() to compare numbers.
  • = stores and gives the stored value, so if (x = 5) always runs; compare with ===, and add parentheses when precedence is in doubt.
  • A bigint mixed with a number, a symbol joined to text and an object with no text form make an operator throw a TypeError.
  • Go deeper: Interview Pack, output puzzles, the questions and the bug gallery (Pro).

Next, lesson 03 converts on purpose in six full programs, starting with a calculator that reads two numbers and an operator.

End of lesson 2

Mark it done, and your progress moves with you.

Next: Full Programs: Converting on Purpose