Episode 1 — Fundamentals / 1.18 — Operators and Type System

1.18.b — Comparison Operators

In one sentence: Comparison operators test relationships between values and return booleans — but the critical difference between == (loose, with coercion) and === (strict, no coercion) is the single most important operator distinction every JavaScript developer must master.

Navigation: ← 1.18.a — Arithmetic Operators · 1.18.c — Logical Operators →


1. Equality operators overview

OperatorNameCoercion?Recommendation
===Strict equalityNoDefault choice
!==Strict inequalityNoDefault choice
==Loose (abstract) equalityYesAvoid unless intentional
!=Loose inequalityYesAvoid unless intentional

2. Strict equality === and !==

=== returns true only when both the type and the value are the same. No conversion happens.

console.log(5 === 5);          // true
console.log(5 === "5");        // false  (number vs string)
console.log(true === 1);       // false  (boolean vs number)
console.log(null === undefined); // false  (different types)
console.log(null === null);    // true
console.log(undefined === undefined); // true

!== is the negation of ===:

console.log(5 !== "5");   // true   (different types)
console.log(5 !== 5);     // false

Rule: Use === and !== as your default for all comparisons.


3. Loose equality == — the abstract equality algorithm

== performs type coercion before comparing. The ECMAScript spec defines a multi-step algorithm (simplified below):

Step-by-step (simplified)

  1. Same type? Compare directly (like ===).
  2. null == undefined? Return true (special rule).
  3. Number vs String? Convert the string to a number, then compare.
  4. Boolean vs anything? Convert the boolean to a number (true → 1, false → 0), then restart.
  5. Object vs primitive? Call ToPrimitive on the object, then restart.
// Step 3: string → number
console.log(5 == "5");         // true   ("5" → 5)
console.log(0 == "");          // true   ("" → 0)
console.log(0 == "0");         // true   ("0" → 0)

// Step 4: boolean → number
console.log(true == 1);        // true   (true → 1)
console.log(false == 0);       // true   (false → 0)
console.log(true == "1");      // true   (true→1, "1"→1)
console.log(false == "");      // true   (false→0, ""→0)

// Step 2: null/undefined special case
console.log(null == undefined); // true   (special rule)
console.log(null == 0);         // false  (null only == undefined)
console.log(null == "");        // false
console.log(null == false);     // false

// Step 5: object → primitive
console.log([1] == 1);          // true   ([1].toString()="1", "1"→1)
console.log([""] == 0);         // true   ([""]. toString()="", ""→0)

The coercion table (common pairs)

ExpressionResultWhy
0 == falsetruefalse → 0
"" == falsetruefalse → 0, "" → 0
"0" == falsetruefalse → 0, "0" → 0
"" == 0true"" → 0
"0" == 0true"0" → 0
"" == "0"falseSame type (string), different values
null == undefinedtrueSpecial rule
null == 0falsenull only == undefined
NaN == NaNfalseNaN never equals anything
[] == falsetrue[] → "" → 0, false → 0
[] == 0true[] → "" → 0

Notice the inconsistency: "" == false is true, "0" == false is true, but "" == "0" is false. This is why == is dangerous.


4. Why === should be your default

  1. Predictable — no hidden conversions, no surprises.
  2. Readable — the reader knows you mean "same type AND value."
  3. Linters enforce it — ESLint's eqeqeq rule flags == usage.
  4. Performance — marginally faster (no coercion step), though negligible.

The one acceptable use of ==

// Checking for null OR undefined in one shot
if (value == null) {
  // value is null or undefined (and nothing else)
}
// Equivalent to:
if (value === null || value === undefined) { ... }

Some style guides allow == null because the semantics are clear and useful. Everything else should use ===.


5. Relational operators: >, <, >=, <=

These compare values and return booleans. They do coerce types.

console.log(10 > 5);      // true
console.log(10 < 5);      // false
console.log(10 >= 10);    // true
console.log(10 <= 9);     // false

Number comparisons

console.log(3 > 2);     // true
console.log(-1 > -5);   // true
console.log(0 > -0);    // false  (0 and -0 are equal)

String comparisons (lexicographic / Unicode order)

Strings are compared character by character using Unicode code points:

console.log("apple" < "banana");   // true   (a < b)
console.log("abc" < "abd");        // true   (c < d at position 2)
console.log("a" < "A");            // false  (a=97, A=65 — lowercase is higher)
console.log("10" < "9");           // true   ("1"=49 < "9"=57 — string comparison!)

Gotcha: "10" < "9" is true because it compares as strings, not numbers. If you want numeric comparison, convert first:

console.log(Number("10") < Number("9"));  // false (correct)

Mixed type comparisons

console.log("5" > 3);         // true   ("5" → 5)
console.log(true > 0);        // true   (true → 1)
console.log(null > 0);        // false  (null → 0, 0 > 0 is false)
console.log(null >= 0);       // true   (null → 0, 0 >= 0 is true)
console.log(null == 0);       // false  (== has special null rules!)
console.log(undefined > 0);   // false  (undefined → NaN)
console.log(undefined < 0);   // false  (NaN comparisons are always false)
console.log(undefined == 0);  // false

Warning: null >= 0 is true but null > 0 is false and null == 0 is false. This inconsistency exists because >= uses numeric coercion but == has its own algorithm.


6. Edge cases every developer must know

NaN comparisons

console.log(NaN === NaN);   // false
console.log(NaN == NaN);    // false
console.log(NaN > 0);       // false
console.log(NaN < 0);       // false
console.log(NaN >= 0);      // false
// NaN makes EVERY comparison false (except !=, !==)
console.log(NaN !== NaN);   // true

0 vs -0

console.log(0 === -0);          // true   (spec says they're equal)
console.log(0 == -0);           // true
console.log(Object.is(0, -0));  // false  (Object.is distinguishes them)
console.log(1 / 0);             // Infinity
console.log(1 / -0);            // -Infinity (they ARE different!)

null vs undefined

console.log(null == undefined);    // true   (special rule)
console.log(null === undefined);   // false  (different types)
console.log(null == false);        // false  (null only == undefined)
console.log(undefined == false);   // false

7. Object.is() for edge cases

Object.is() works like === but handles two edge cases differently:

Comparison===Object.is()
NaN vs NaNfalsetrue
0 vs -0truefalse
Everything elseSameSame
console.log(Object.is(NaN, NaN));   // true
console.log(Object.is(0, -0));      // false
console.log(Object.is(5, 5));       // true
console.log(Object.is("a", "a"));   // true
console.log(Object.is(null, null)); // true

When to use Object.is: Rarely in everyday code. Useful in libraries, polyfills, or when you specifically need to distinguish 0 from -0 or detect NaN reliably.


8. Comparison operators in practice

Input validation

function validateAge(input) {
  const age = Number(input);
  if (Number.isNaN(age)) {
    return "Please enter a valid number";
  }
  if (age < 0 || age > 150) {
    return "Age must be between 0 and 150";
  }
  return `Age accepted: ${age}`;
}

Sorting with comparisons

const scores = [85, 92, 78, 95, 88];
// Sort ascending
scores.sort((a, b) => a - b);      // [78, 85, 88, 92, 95]
// Sort descending
scores.sort((a, b) => b - a);      // [95, 92, 88, 85, 78]

Clamping a value

function clamp(value, min, max) {
  if (value < min) return min;
  if (value > max) return max;
  return value;
}
console.log(clamp(15, 0, 10));  // 10
console.log(clamp(-5, 0, 10));  // 0
console.log(clamp(7, 0, 10));   // 7

Key takeaways

  1. === checks type and value with no coercion — use it as your default.
  2. == coerces types via the abstract equality algorithm — produces many surprising results.
  3. The only widely accepted use of == is value == null to check for null or undefined.
  4. String comparison is lexicographic (Unicode order) — "10" < "9" is true.
  5. NaN is not equal to anything, including itself — use Number.isNaN().
  6. null >= 0 is true but null == 0 is false — relational and equality operators use different coercion rules.
  7. Object.is() handles the NaN and -0 edge cases that === does not.

Explain-It Challenge

Explain without notes:

  1. Why does "" == false return true? Walk through the coercion steps.
  2. What are the three results of null > 0, null == 0, and null >= 0? Explain the inconsistency.
  3. Why is NaN === NaN false, and how would you check if a variable holds NaN?
  4. Give one scenario where Object.is() behaves differently from ===.
  5. Why should === be your default comparison operator?

Navigation: ← 1.18.a — Arithmetic Operators · 1.18.c — Logical Operators →