== vs === — what does coercion actually do?
=== compares value + type; == coerces the operands following a fixed algorithm before comparing. Always use === except for one defensible idiom: `x == null` to check for null OR undefined.
== (Abstract Equality) coerces operands to a common type before comparing, following an explicit algorithm in the spec. === (Strict Equality) returns false if the types differ — no coercion. The interview question is: do you know why === is the default, and where == is actually defensible?
The == coercion algorithm (the parts that bite).
null == undefined→true. They equal each other and only each other.NaN == NaN→false(useNumber.isNaNorObject.is).number == string→ string is coerced to number:"5" == 5→ true;"" == 0→ true;" " == 0→ true (whitespace coerces to 0).boolean == anything→ boolean is coerced to number first:true == 1(true),false == 0(true),true == "1"(true),true == "true"(false because "true" → NaN).object == primitive→ object is converted viaToPrimitive(callsvalueOfthentoString):[1] == 1(true),[1, 2] == "1,2"(true).
These rules produce the famous wat-list: [] == false (true), [] == ![] (true), "0" == false (true) but "0" == "" (false).
=== rules (simple). Same type or false. Same value (with one exception: NaN === NaN is false). +0 === -0 is true. Object.is differs only on these two: Object.is(NaN, NaN) is true and Object.is(+0, -0) is false.
The one defensible == use. x == null is true for both null and undefined — the most concise way to check "is this nullish." Otherwise, ESLint rules (eqeqeq) ban == outright.
Modern alternatives to coercion-based checks.
- Nullish:
x ?? default(null/undefined only) vsx || default(any falsy). - Optional chaining:
x?.fooshort-circuits on null/undefined. - Coercion: be explicit —
Number(x),String(x),Boolean(x). - NaN check:
Number.isNaN(x). The globalisNaNfirst coerces (isNaN("foo")→ true, surprising).
Reference equality. Both == and === compare objects by reference, not contents: {a:1} === {a:1} is false. For deep equality use JSON.stringify (limited), Lodash _.isEqual, or roll your own.
Output puzzles you should be able to walk through.
[] == ![] // true: ![] → false → 0; [] → "" → 0; 0 == 0
"" == 0 // true: "" → 0
" \t\n " == 0 // true: whitespace string → 0
null == 0 // false: null only equals undefined
null >= 0 // true: comparison operators coerce; null → 0
"0" == false // true: false → 0; "0" → 0
0.1 + 0.2 === 0.3 // false: floating pointBottom line. Use === everywhere except x == null. Prefer explicit conversion (Number(x)) over implicit coercion. ESLint's eqeqeq: ["error", "always", { null: "ignore" }] codifies this exactly.
Code
Follow-up questions
- •Why is `[] == ![]` true?
- •What's the difference between Object.is and ===?
- •When is == defensible?
- •Why does `0.1 + 0.2 !== 0.3`?
Common mistakes
- •Using == thinking it's faster — it's not, and it's footgun-laden.
- •Using `||` for defaults when you mean `??` — wipes valid 0/'' values.
- •Comparing objects with === expecting deep equality.
- •Using global isNaN instead of Number.isNaN.
Performance considerations
- •=== avoids the coercion machinery — slightly faster in microbenchmarks; irrelevant in practice.
- •Don't reach for Lodash isEqual on a hot path; write a domain-specific comparator.
Edge cases
- •NaN !== NaN — only Number.isNaN / Object.is detect it.
- •+0 === -0 but Object.is differentiates.
- •Document.all behaves as undefined in == — historical browser quirk.
Real-world examples
- •Almost every codebase enforces eqeqeq via ESLint; React source uses Object.is for shallow equality.