Equality and Coercion
Date: 2026-09-28
===compares without converting.==runs a short, fixed algorithm that converts both sides towards numbers — learn the algorithm once and every “wat” example becomes predictable. Then use===anyway, except for== null.
Coercion is JavaScript converting a value to another type implicitly because an operator requires it; loose equality (==) is the comparison that coerces its operands first, and strict equality (===) is the one that doesn’t.
The == algorithm
Roughly the spec’s IsLooselyEqual, in order:
x == y
1 same type? → compare as ===
2 null / undefined on both sides? → true (they equal each other and NOTHING else)
3 number vs string → string to number, retry
4 a boolean on either side → boolean to number (true→1, false→0), retry
5 object vs primitive → object to primitive, retry
(Symbol.toPrimitive, else valueOf, else toString)
6 BigInt vs number / string → compare the mathematical values
anything else → false
Worked — [] == false:
[] == false
[] == 0 step 4: false → 0
'' == 0 step 5: [] → [].toString() → ''
0 == 0 step 3: '' → Number('') → 0
true
Worked — the contradiction everyone hits:
'0' == false // true — '0' → 0, false → 0
if ('0') {} // runs — a non-empty string is truthy== and truthiness are different conversions. == heads for numbers; if asks “is this falsy?” Nothing guarantees they agree, and here they don’t.
Truthiness
The falsy list is short and closed — everything else, including '0', 'false', [] and {}, is truthy:
false 0 -0 0n '' null undefined NaN
(Plus document.all, a legacy browser object kept falsy for web compatibility.)
Three equality algorithms, not two
NaN vs NaN | 0 vs -0 | Used by | |
|---|---|---|---|
=== (strict) | false | true | ===, indexOf, switch |
| SameValueZero | true | true | includes, Map / Set keys |
Object.is (SameValue) | true | false | Object.is, React’s state comparison |
So [NaN].includes(NaN) is true while [NaN].indexOf(NaN) is -1.
Other coercions that bite
+with a string anywhere concatenates:'5' + 1is'51';'5' - 1is4. Form input values are always strings, soqty + 1on an input value appends- Default
sort()compares as strings:[10, 9, 1].sort()gives[1, 10, 9]. Always pass a comparator for numbers ||for defaults treats0and''as missing:quantity || 1turns a real zero into one.??only replacesnull/undefinedNumber()vsparseInt()—Number('12px')isNaN;parseInt('12px', 10)is12. Neither is wrong; they answer different questions- Object keys are strings —
obj[1]andobj['1']are the same property; aMapkeeps1and'1'distinct
The rule in practice
- Default to
===. It’s never surprising x == nullis the one defensible==— it’s true for exactlynullandundefined(step 2), which is usually the question being asked. Most lint configs allow it as an exception — Linting- Convert explicitly at boundaries —
Number(input.value)once, where the string enters, rather than trusting operators later. TypeScript catches much of this at compile time, not at the boundary — Type Systems
vs PHP: same idea, different table. PHP 8 changed how numbers compare with non-numeric strings (0 == 'abc' became false), which JavaScript never had to fix because it always converted the string to NaN — PHP Type Juggling.