JavaScript has two kinds of equality and an automatic type-conversion system that runs silently. Understanding both is essential for reading legacy code and for answering the coercion puzzles that appear in screens.
Strict vs loose equality
=== (strict) compares type and value โ no coercion. == (loose) first coerces both operands to a common type, then compares. The coercion rules are complex enough that you should use === by default and only reach for == when you explicitly want type flexibility (e.g., value == null to catch both null and undefined).
console.log. Async logs (setTimeout/Promise) appear in real execution order.The [] == ![] case is the classic puzzle. ![] evaluates to false (arrays are truthy), so you get [] == false. Then [] converts to "", which converts to 0, and false converts to 0. Both sides are 0 โ true. This is why you avoid ==.
The falsy seven
Exactly seven values are falsy in JavaScript โ everything else is truthy:
false, 0, -0, 0n, "", null, undefined, NaN
Common surprises: [] and {} are truthy (even empty). "0" is truthy. new Boolean(false) is truthy (itโs an object). document.all is a historical exception but ignore that.
console.log. Async logs (setTimeout/Promise) appear in real execution order.Type coercion rules
When JS needs to convert a value, it uses one of three abstract operations:
- ToNumber:
""โ0,"3"โ3,nullโ0,undefinedโNaN,trueโ1,falseโ0,[]โ0,[1]โ1,[1,2]โNaN. - ToString:
nullโ"null",undefinedโ"undefined",[]โ"",{}โ"[object Object]", numbers use their numeric string. - ToPrimitive: for objects, calls
[Symbol.toPrimitive], then.valueOf(), then.toString().
The + operator is the sneakiest โ it prefers string concatenation if either operand is a string:
1 + "2" // "12" โ string wins
1 + 2 + "3" // "33" โ left to right: 3 then "3"
"3" + 2 + 1 // "321" โ "3" first poisons everything
+"3" // 3 โ unary + triggers ToNumber
+[] // 0
+{} // NaN
typeof and the null bug
typeof returns a string describing the type โ with one infamous bug:
typeof 42 // "number"
typeof "hi" // "string"
typeof true // "boolean"
typeof undefined // "undefined"
typeof Symbol() // "symbol"
typeof 42n // "bigint"
typeof function(){} // "function"
typeof {} // "object"
typeof [] // "object" โ arrays are objects
typeof null // "object" โ the bug, baked in since 1995
null is not an object โ itโs its own primitive type. The typeof null === "object" result is a historical bug that canโt be fixed without breaking the web. To check for null: use value === null.
NaN โ the only value not equal to itself
NaN (Not a Number) is the result of invalid numeric operations. Itโs infectious and bizarre:
NaN === NaN // false โ NaN is never equal to anything including itself
isNaN("hello") // true โ but isNaN coerces first! ("hello" โ NaN)
Number.isNaN("hello") // false โ strict, only true for actual NaN values
Number.isNaN(NaN) // true โ the right check
Number.isFinite(Infinity) // false
Object.is(NaN, NaN) // true โ Object.is uses SameValue, not ===
Always use Number.isNaN(), not global isNaN().
Object.is โ SameValue equality
Object.is(a, b) is like === but handles two edge cases differently:
Object.is(NaN, NaN) // true (=== returns false)
Object.is(+0, -0) // false (=== returns true)
This is what React uses internally to compare state values for bail-outs.
=== everywhere by default โ it never coerces. == triggers abstract equality coercion: operands are converted via ToNumber/ToString/ToPrimitive until they share a type. The seven falsy values are false, 0, -0, 0n, "", null, undefined, and NaN. typeof null is 'object' โ a legacy bug. NaN is not equal to itself, so use Number.isNaN() to detect it. Object.is is stricter than === and correctly handles NaN and -0.โ