Equality and type coercion
Predict == vs === behavior, deep vs shallow equality, and the specific rules NaN and Object.is change.

WHAT YOU WILL BE ABLE TO DO
Learning outcomes
- Explain equality and type coercion in plain language.
- Connect the behavior to the underlying browser or framework model.
- Implement the core pattern and reason through edge cases.
- Answer common follow-ups without relying on memorized phrases.
01Explain it simply
=== compares value and type with no conversion; == first coerces operands to a common type using a specific set of rules, then compares. Both only compare primitives directly and compare objects by reference, never by their contents — that's what 'deep equality' has to solve separately.
One-line definition: Predict == vs === behavior, deep vs shallow equality, and the specific rules NaN and Object.is change.
02Mental model
Think of == as running a documented coercion algorithm before comparing, not as looser or buggy equality. The rules are specific: null == undefined is true, numbers and strings coerce the string to a number, and booleans coerce to numbers first. Memorizing the table matters less than knowing === removes the whole problem by refusing to convert.
03Step by step
- Default to === and !== to avoid relying on the coercion table.
- Remember object or array comparison with == or === is always by reference, never by contents.
- Use a deep-equality check, a structural comparison, when contents matter, not reference identity.
- Know NaN !== NaN, so use Number.isNaN() to detect it.
- Know Object.is differs from === only for NaN, which it treats as equal to itself, and signed zero, where -0 and +0 are different.
04Working example
0 == '0' // true — string coerced to number0 == [] // true — array coerced to '' then to 00 == '\t' // truenull == undefined // true, but null == 0 is false
NaN === NaN // falseObject.is(NaN, NaN) // trueObject.is(0, -0) // false, while 0 === -0 is trueEach == case follows the abstract equality algorithm's defined coercion steps rather than random behavior — but the fact that three visually different values, 0, '0', and [], all compare loosely equal to 0 is exactly why === is the safer default. Object.is exists specifically for the two cases where === disagrees with 'are these the same value.'
05Where it is used
- Choosing === as the default comparison operator in code review
- Detecting NaN correctly with Number.isNaN instead of x !== x tricks
- Comparing two objects or arrays for the same contents with a structural deep-equal function
- Explaining why React's dependency arrays and memoization use reference equality, not deep equality, by default
06Common mistakes
- Using == out of habit and hitting a coercion surprise like '' == 0 being true
- Assuming Object.is is a general replacement for === — it only differs on NaN and -0/+0
- Comparing two arrays or objects with === and being surprised identical-looking data isn't 'equal'
- Writing a custom deep-equal that doesn't handle NaN, -0, or circular references correctly
07Interview answer
Walk through the specific coercion steps for a tricky == example instead of just saying coercion is unpredictable — showing you know the actual rules, and that === sidesteps them entirely, is the stronger signal.
Why does [] == false evaluate to true?
The abstract equality algorithm coerces both sides toward numbers: false becomes 0, and the array is first converted to a primitive via toString, producing an empty string, which then converts to the number 0 — so the comparison becomes 0 == 0, which is true.
DDConcept deep dives
Deep dive 1
== runs a defined algorithm, it isn't random
The abstract equality comparison algorithm specifies exactly how each type pairing is handled: null and undefined are loosely equal only to each other, number-string pairs coerce the string to a number, and boolean operands coerce to numbers before further comparison. Every seemingly odd == result, like '' == 0 or [] == false, is traceable to a specific documented step — the practical lesson isn't memorizing the table, it's recognizing that === exists precisely to skip needing to.
- null == undefined is a deliberate special case, not a general coercion outcome.
- An object operand is first converted to a primitive before the numeric coercion rules apply.
- Reciting '== is unpredictable' is weaker than tracing the actual steps for a specific example.
Deep dive 2
=== and == still only compare references for objects
Neither operator performs any structural comparison for objects, arrays, or functions — both simply check whether the two operands are literally the same reference in memory. Two arrays with identical contents, or two otherwise-equal plain objects built separately, are never equal under either operator. This is exactly the gap that structural or deep-equality utilities exist to fill, and it's also the underlying reason React's own memoization and dependency-array checks default to reference equality rather than trying to compare contents.
- A new array or object literal is always a new reference, even with identical contents.
- Frameworks default to reference equality for performance; deep equality is comparatively expensive.
- A deep-equal implementation must define its own rules for NaN, -0, and circular structures.
{ a: 1 } === { a: 1 } // false — different referencesJSON.stringify({ a: 1 }) === JSON.stringify({ a: 1 }) // true, but fragiledeepEqual({ a: 1 }, { a: 1 }) // true, via an actual structural comparisonJSON.stringify comparison is a common shortcut, but it breaks on key order, undefined values, functions, and circular references — a real deep-equal utility handles those cases explicitly.
Deep dive 3
NaN and Object.is close two specific gaps in ===
IEEE 754 defines NaN to not equal itself under any standard comparison, which is mathematically consistent, since NaN represents 'not a specific number,' but is a frequent source of bugs when someone expects === to detect it. Object.is corrects exactly this one case, treating NaN as equal to itself, and one other case, treating +0 and -0 as different — beyond those two specific exceptions, Object.is and === behave identically, so it's not a general-purpose equality upgrade.
- Number.isNaN() is the idiomatic way to test for NaN, not x !== x tricks.
- The -0 vs +0 distinction rarely matters outside of numerical edge cases like division results.
- Object.is is what React's own reconciliation-adjacent internals use in a few specific low-level checks, not a replacement for everyday ===.
QAInterview questions and model answers
Attempt each answer aloud before opening it. The model answer shows the depth and precision expected in an interview; it is not a script to memorize.
Beginner · Coding · 1 min · Question 1What is the core difference between == and ===?Open model answer
Model answer
=== compares value and type with no conversion, so operands of different types are simply unequal. == first coerces both operands to a common type using the abstract equality algorithm's defined rules, then compares them.
1 === '1'; // false — different types, no conversion1 == '1'; // true — '1' coerced to number 10 == false; // true — both coerce to 0null == undefined; // true (special-cased)Intermediate · Conceptual · 1 min · Question 2Why is null == undefined true but null == 0 false?Open model answer
Model answer
The abstract equality algorithm specifically defines null and undefined as loosely equal only to each other and to themselves — they are deliberately excluded from the numeric coercion path that would otherwise compare them to 0.
Open question page →Intermediate · Coding · 1 min · Question 3Why does NaN === NaN evaluate to false?Open model answer
Model answer
By IEEE 754 floating-point specification, NaN is defined to not equal itself under any equality comparison, strict or loose. Number.isNaN() or Object.is() must be used to correctly detect a NaN value.
NaN === NaN; // falseNumber.isNaN(NaN); // true[1, NaN].includes(NaN); // true — includes uses SameValueZero[1, NaN].indexOf(NaN); // -1 — indexOf uses ===Intermediate · Coding · 1 min · Question 4How does Object.is differ from ===?Open model answer
Model answer
It behaves identically to === for almost every value, but differs in exactly two cases: Object.is(NaN, NaN) is true where NaN === NaN is false, and Object.is(0, -0) is false where 0 === -0 is true.
Object.is(NaN, NaN); // true (=== gives false)Object.is(0, -0); // false (=== gives true)Object.is(1, 1); // true (same as ===)Intermediate · Conceptual · 1 min · Question 5Why do == and === both return false when comparing two different arrays with identical contents?Open model answer
Model answer
Neither operator performs structural comparison for objects or arrays — both compare object references, so two distinct array instances are never equal to each other regardless of operator, even with identical contents.
Open question page →Intermediate · Conceptual · 1 min · Question 6What's a common real-world reason to write a deep-equal utility instead of relying on ===?Open model answer
Model answer
Comparing two API response objects, or two pieces of derived state, for the same logical content rather than the same reference — for example, deciding whether to skip a re-render because the data hasn't meaningfully changed, even though a new object was created.
Open question page →Advanced · Conceptual · 1 min · Question 7Does Array.prototype.includes use === or something else to find a match?Open model answer
Model answer
It uses SameValueZero, which behaves like === except it treats NaN as equal to itself — this is why arr.includes(NaN) can find NaN in an array while arr.indexOf(NaN) cannot, since indexOf uses strict equality.
Open question page →Intermediate · Conceptual · 1 min · Question 8Why does comparing two different Date objects with the same timestamp using === return false?Open model answer
Model answer
Date objects are objects, so === compares references, not their internal time value; two distinct Date instances are never === to each other even representing the identical moment — use .getTime() to compare their values instead.
Open question page →Advanced · Conceptual · 1 min · Question 9What does the ! operator's implicit boolean coercion cause in !someArray.length?Open model answer
Model answer
It coerces the length number to a boolean using truthiness rules first, so this correctly checks for an empty array since 0 is falsy and any positive length is truthy — the double negation !! pattern is often used to make this explicit boolean conversion visible.
Open question page →Beginner · Conceptual · 1 min · Question 10Why is typeof NaN 'number' and not something like 'NaN'?Open model answer
Model answer
NaN is defined as a special value within the IEEE 754 number type, representing an invalid numeric result — it is still fundamentally of the number type, just one specific value within it that fails to equal itself.
Open question page →SCScenario questions
Scenario 1
A code reviewer flags if (value == null) in a pull request, assuming it's a leftover loose-equality bug that should be value === null.
- Recognize this specific pattern as a deliberate, idiomatic use of ==.
- Explain that value == null is true for both null and undefined.
- Contrast it with value === null, which would miss the undefined case.
- Note that this is one of the few places == is commonly preferred over ===.
Reveal worked answer
I would explain that value == null is actually a well-known idiom, not a mistake — because null == undefined is true by specification, this single check covers both null and undefined in one comparison, which is exactly what's usually wanted for an 'is this value missing' check. Using === null here would silently miss the undefined case, requiring a second explicit check.