DeepFrontend
Practice ArenaBlogSign in
Go Pro
DeepFrontend

Interview-grade learning paths and hands-on practice for mid-to-senior engineers. Master internals, patterns, and system architecture with runnable code.

All systems operational

Core Courses

  • -JavaScript Internals
  • -React Reconciliation
  • -TypeScript rigor
  • -Next.js Caching
  • -Node.js Event Loop
  • -System Design
  • -Web Security
  • -CSS & Page Layouts

Explore

  • Learning Paths
  • Practice Arena
  • Pricing Options
  • HTML Sitemap

Resources

  • Privacy Policy
  • Terms of Service
  • Email Support

© 2026 DeepFrontend. All rights reserved.

Expert learning environments for web engineering teams.

Home/JavaScript/Coercion & Equality (== vs ===)
intermediate20 min read·Updated Jun 2025

Coercion & Equality (== vs ===)

`==` isn't random — it follows a precise, spec-defined algorithm. Once you know the algorithm, every 'wat' example (`[] == false`, `'' == 0`) stops being magic and starts being predictable.

coercionequalitytype-conversiontoprimitivefundamentals

Knowledge Check

5 questions · pass at 70%

0/5
easypredict output

1. What does this evaluate to?

code
console.log('' == 0);
console.log('' === 0);

Interview Questions

2 questions

Cheatsheet

Download-ready reference
Cheatsheet
== triggers coercion. === never does.

null == undefined   → true   (ONLY this special case, hardcoded)
null == 0            → false  (null does NOT convert to a number)
NaN == NaN            → false  (NaN never equals itself — use Number.isNaN)

ToPrimitive(obj): try Symbol.toPrimitive → valueOf() → toString()

== conversion order (roughly):
  boolean            → number, then re-compare
  number <-> string   → string becomes number
  object <-> primitive → object runs ToPrimitive, then re-compare

Rule of thumb:
  Always use === / !==
  Only exception: `value == null` to test "null or undefined" in one check
  Never compare to NaN with ==/=== — use Number.isNaN(x)

Related topics

JavaScript Fundamentals`this` Binding, call/apply/bindPro

On this page

20m
Knowledge CheckInterview QuestionsCheatsheet

Concept#

The beginner framing: JavaScript will convert values between types automatically in many situations — string concatenation with +, comparisons with ==, conditions in if. This is called coercion, and it comes in two flavors: explicit (you call Number(x), String(x), Boolean(x) yourself) and implicit (the language does it for you, silently, as part of some other operation).

The precise mental model: implicit coercion isn't random — every operator has an exact, spec-defined algorithm for what it does with mismatched types. == (Abstract Equality Comparison) and + both lean on an internal operation called ToPrimitive, which converts an object to a primitive before any further comparison happens. Once you know this one operation, the entire family of "wat" examples becomes mechanically predictable instead of magic.

The engine-level view — ToPrimitive(object, hint):

  1. If the object has a Symbol.toPrimitive method, call it (most objects don't define this).
  2. Otherwise, try valueOf() — if it returns a primitive, use that.
  3. Otherwise, try toString() — use whatever primitive that returns.
const obj = {
  valueOf() { return 42; },
  toString() { return "obj-string"; },
};
 
console.log(obj + "");       // "42" — valueOf() wins, then coerced to string for +
console.log(`${obj}`);        // "obj-string" — template literals hint "string", skip to toString
console.log([] + "");         

The == algorithm, traced#

== between two different types roughly follows this decision tree (simplified from the spec):

  • null == undefined → true (a special case, hardcoded — the ONLY thing null loosely equals besides itself)
  • null == anything else → false, always
  • number == string → convert the string to a number, then compare
  • boolean == anything → convert the boolean to a number (true → 1, false → 0), then re-run the comparison
[] == false;
// 1. false → 0 (boolean rule)
// 2. [] → ToPrimitive → "" (empty array's toString) → "" → 0 (string-to-number)
// 3. 0 == 0 → true
Trace: '' == 0
'' == 0
// string == number → convert string to number
// Number('') === 0
// 0 == 0 → true

Try It#

Predict each result, then check yourself.

console.log(null == undefined);
console.log(null === undefined);
console.log(NaN == NaN);
console.log([1] == 1);
console.log("0" == false);
console.log(null
Solution
true    — the one hardcoded special case
false   — different types, strict equality never coerces
false   — NaN is never equal to anything, including itself (use Number.isNaN)
true    — [1] → ToPrimitive → "1" → 1 (number) → 1 == 1
true    — "0" → 0 (number), false → 0 (boolean) → 0 == 0
false   — null ONLY == undefined and itself; it does NOT coerce to 0

The last one trips up almost everyone: null == 0 is false because null's equality rule is a special hardcoded case that short-circuits before the normal type-coercion machinery ever runs — it never gets converted to a number at all.


Implement It Yourself#

Implement a simplified looseEquals that reproduces a few of =='s real coercion rules, to internalize the algorithm rather than just memorizing outcomes:

function looseEquals(a, b) {
  // Same type → behaves like ===
  if (typeof a === typeof b) return a === b;
 
  // The null/undefined special case
  if (a == null && b == null) return true; // catches both null and undefined
  if (a == null ||















This is a simplification of the real spec algorithm (the actual ToPrimitive is more involved), but reproducing the major branches yourself is the fastest way to stop being surprised by ==.


Common Mistakes#

1. Using == out of habit and getting unpredictable results#

function isEmpty(value) {
  return value == ""; // "fixes" empty strings, but ALSO matches 0, false, null...
}
isEmpty(0); // true — probably not what you meant!

=='s coercion net is wider than most people intend. Use === and check explicitly for the cases you actually care about (value === "" || value == null).

2. Assuming NaN behaves like a normal value#

const results = [1, NaN, 3];
results.includes(NaN); // true — includes() uses SameValueZero, NOT ===
results.indexOf(NaN);  // -1  — indexOf DOES use ===, so it "fails" to find it

NaN !== NaN under both == and === — it's the one value in JavaScript that is never equal to itself under strict equality. Use Number.isNaN(x) or Object.is(x, NaN) to test for it reliably, and be aware different built-in methods use different equality algorithms internally.

3. Relying on implicit coercion in template literals or concatenation for objects#

console.log(`User: ${user}`); // "User: [object Object]" if user has no toString/valueOf override

Without a custom toString() or Symbol.toPrimitive, objects stringify to the unhelpful "[object Object]". If you want meaningful string output, define toString() explicitly rather than relying on the default.


Best Practices#

  • Use ===/!== by default. Reserve == for the one genuinely useful case: value == null as a concise check for "null or undefined" (catches both in one comparison).
  • Never compare against NaN with ==/===. Always use Number.isNaN().
  • Define toString()/valueOf()/Symbol.toPrimitive on your own classes/objects if they'll ever be coerced (logged, concatenated, compared) — don't leave it to .

Performance Tips#

  • Coercion itself is essentially free — a few extra internal method calls (valueOf/toString) that modern engines inline and optimize well. This is not a performance topic; the entire concern here is correctness and readability, not speed.
  • If you're calling toString()/valueOf() on your own objects in a hot path (e.g., inside a tight loop doing string concatenation), make sure those methods are cheap — a toString() that does expensive work will run on every implicit coercion, which is easy to trigger accidentally.

// "" — array's default toString() joins elements with commas; empty array → ""
console.log([1, 2] + ""); // "1,2" — same mechanism
  • object == primitive → run ToPrimitive on the object, then re-run the comparison
  • ==
    0
    );
    b
    ==
    null
    )
    return
    false
    ;
    // boolean → number
    if (typeof a === "boolean") return looseEquals(Number(a), b);
    if (typeof b === "boolean") return looseEquals(a, Number(b));
    // object → primitive (simplified ToPrimitive: just try valueOf then String())
    if (typeof a === "object") return looseEquals(a.valueOf() !== a ? a.valueOf() : String(a), b);
    if (typeof b === "object") return looseEquals(a, b.valueOf() !== b ? b.valueOf() : String(b));
    // number <-> string
    return Number(a) === Number(b);
    }
    looseEquals([], false); // true
    looseEquals("0", false); // true
    looseEquals(null, 0); // false
    "[object Object]"
  • Be explicit with conversions (Number(x), String(x), Boolean(x), or !!x) rather than relying on an operator's implicit behavior — it documents intent for the next reader.