Booleans, Conditions, and Truthiness
if and else, why === is the comparison you use, and the falsy values that make an innocent condition reject a legitimate zero or empty string.
if and else, why === is the comparison you use, and the falsy values that make an innocent condition reject a legitimate zero or empty string.
A checkout applies a ten percent discount when no discount was
specified. The code says if (!discount) discount = 0.10; and
looks entirely reasonable. It works for every customer except
the ones entitled to no discount at all, whose 0 is treated as
"nothing was given".
Every program you have written so far runs straight through. This lesson is where they start choosing — and where you meet the set of values this language treats as false, which is broader than you expect and produces exactly that bug. By the end you will write conditions that mean what they say.
const photoCount = 400;
if (photoCount > 100) {
console.log("That is a lot of photos");
}The condition goes in parentheses; the block runs only when it holds. Braces mark what is inside:
if (photoCount > 100) {
console.log("That is a lot of photos"); // only when true
console.log("Processing in batches"); // also only then
}
console.log("Done"); // alwaysThe braces are optional for a single statement and you should write them anyway. Omitting them means adding a second line later silently puts it outside the condition — a real bug, in a diff that looks correct.
else covers the other case, and else if chains more tests:
if (photoCount === 0) {
console.log("Nothing to do");
} else if (photoCount < 50) {
console.log("Processing all at once");
} else {
console.log("Processing in batches");
}Only the first match runs, which is why the second test does not also need "and 50 or more".
a === b // equal
a !== b // not equal
a > b a < b a >= b a <= bThree equals signs, not two. There is an older == that
converts its operands before comparing, and the conversions are
surprising:
0 == ""; // true
0 == "0"; // true
null == undefined; // true
[] == false; // trueAlways use === and !==. The one legitimate exception is
value == null, which is true for both null and undefined
and is a genuinely useful shorthand — the next lesson covers
why.
Comparing text is character by character, and case matters:
"apple" < "banana"; // true
"Ana" === "ana"; // false
"Ana".toLowerCase() === "ana"; // trueif (photoCount > 0 && photographerIsKnown) { ... } // both
if (fileMissing || fileEmpty) { ... } // either
if (!isProcessed) { ... } // notThese short-circuit: && stops at the first false part and
|| at the first true one, without evaluating the rest. That
lets a cheap check on the left protect something on the right:
if (photographer !== null && photographer.name === "Ana") { ... }If photographer is null, the right side never runs — and the
crash from asking null for its name never happens. Written
the other way round, it would.
A condition does not have to be a comparison. Any value can be tested, and these are falsy — they behave as false:
false 0 -0 0n "" null undefined NaN
Worth memorising, because it is short and because three of
them — 0, "" and NaN — are values a program legitimately
produces and means.
Including some that look empty
"0" and "false" are non-empty text, so both are truthy.
So are [] and {} — if ([]) runs the block, which
surprises people regularly.
Truthiness lets conditions read pleasantly:
And now the bug from the opening.
Bad — truthiness to check whether a value was supplied.
Good — checking for absence, not for emptiness.
A discount of 0 is falsy, so the first version cannot tell "no
discount, deliberately" from "nothing was passed", and charges
ten percent less to a customer entitled to nothing. The same
trap catches an empty string somebody saved on purpose, and a
quantity of zero.
?? is the nullish coalescing operator, and the difference
between it and || is the whole of this bug.
"Was this provided?"
Supplies the default only for null and undefined.
A deliberate 0, "" or false passes straight through,
because those were provided.
"Is this empty?"
Supplies the default for every falsy value, so a legitimate
0 is replaced.
Correct only when you genuinely mean "treat empty the same as missing".
The same distinction appears when reading through a value that might be absent:
?. is optional chaining: if the thing before it is null
or undefined, the whole expression is undefined and nothing
crashes. Combined with ?? it reads as "the city, or unknown".
Use it where absence is genuinely expected. Sprinkling ?.
everywhere hides the question of why something might be
missing, and TypeScript will already tell you which values can
be — the next lesson is about exactly that.
When both branches produce a value rather than doing work, the conditional operator is more direct:
Condition, ?, the value if true, :, the value if false. It
is an expression, so it can go straight into a template literal:
Nesting them is where it goes wrong. Two levels is already hard
to read, and an if/else if chain is clearer for anything
with three outcomes.
switch compares one value against several possibilities:
break is mandatory and its absence is a classic bug: without
it, execution continues into the next case. That behaviour is
deliberate and occasionally useful — "done" and "archived"
above share a body — but a missing break looks identical to an
intentional one.
switch uses ===, so it compares exactly. Always include a
default, which is where an unexpected value should be noticed
rather than ignored.
Your programs can now take different paths, and you know the
falsy list and the specific bug it causes — a deliberate 0 or
"" overwritten by a default. That is a real bug in real
codebases, and you will now spot it in review.
Next is Null, undefined, and Strict Null Checks, which is where this language earns its reputation. Two kinds of nothing, the crash they cause in most languages, and how turning on one compiler setting converts that entire category of runtime error into something the checker reports before you run anything.
Before you move on, write a small program that asks for a number
and reports something different for negative, zero, small and
large. Then deliberately reproduce the discount bug: give a
function an optional number, default it with ||, and pass 0.
Watching a legitimate zero disappear is what makes ?? stick.
SHAPE
if (condition) { ... } else if (other) { ... } else { ... }
always write the braces - adding a line later is a silent bug
only the first matching branch runs
COMPARING
=== !== always these
== != converts first; 0 == "" is true. Do not use.
value == null the one exception: true for null AND undefined
> < >= <= text compares character by character, case matters
COMBINING
&& || !
short-circuit: x !== null && x.name === "Ana"
FALSY - everything else is truthy
false 0 -0 0n "" null undefined NaN
truthy and surprising: "0" "false" [] {}
THE TRAP
if (!discount) discount = 0.10;
-> a deliberate 0 is overwritten
discount ?? 0.10 only for null/undefined <- "was it given?"
discount || 0.10 for every falsy value <- "is it empty?"
REACHING SAFELY
customer.address?.city undefined instead of a crash
customer.address?.city ?? "unknown"
use where absence is expected, not everywhere
CHOOSING A VALUE
const label = n === 1 ? "photo" : "photos";
do not nest more than one level
SWITCH
compares with ===
`break` is mandatory; falling through looks identical to a bug
stacked cases share a body deliberately
always write a defaultif (photos.length) { ... } // there are some
if (!name) { ... } // empty or missingfunction applyDiscount(total: number, discount?: number): number {
if (!discount) {
discount = 0.10; // "none given, use the default"
}
return total * (1 - discount);
}
applyDiscount(100, 0); // 90 - the customer wanted NO discountfunction applyDiscount(total: number, discount?: number): number {
const rate = discount ?? 0.10;
return total * (1 - rate);
}
applyDiscount(100, 0); // 100 - respects the zeroconst city = customer.address.city;
// crashes if address is nullconst city = customer.address?.city; // undefined instead
const city = customer.address?.city ?? "unknown";const label = photoCount === 1 ? "photo" : "photos";console.log(`${photoCount} ${photoCount === 1 ? "photo" : "photos"}`);switch (status) {
case "pending":
showSpinner();
break;
case "done":
case "archived":
showResult();
break;
default:
showError();
}