Chapter 7

Abstract Equality: Deriving All 12 Rules of ==

[] == ![] evaluates to true in JavaScript. This is not a bug — it is the inevitable result of following 12 equality rules in the specification, step by step.

🔹 Level 1 · What You Need to Know

When Using == is Safe

There is exactly one situation where == is recommended: checking for null or undefined.

// ✅ The only recommended use of ==: check for both null and undefined
if (value == null) {
  // enters when value is null OR undefined
}

// Equivalent to (but more verbose):
if (value === null || value === undefined) {
  // same as above
}

// Practical business example
function getUser(id) {
  const user = db.find(id);
  if (user == null) {       // safe whether find() returns null or undefined
    throw new Error("User not found");
  }
  return user;
}

When You Should Never Use ==

// ❌ Comparing with 0
0 == false     // true  (boolean → number: false → 0)
0 == ""        // true  ("" → number: 0)
0 == null      // false (null only equals undefined)
0 == undefined // false (same)
0 == "0"       // true  ("0" → number: 0)

// ❌ Comparing with ""
"" == false    // true  (boolean → number: false → 0, "" → number: 0)
"" == 0        // true
"" == null     // false

// ❌ Comparing with false (most dangerous — many values == false)
false == 0      // true
false == ""     // true
false == "0"    // true
false == null   // false
false == []     // true  ← shocking!
false == {}     // false ← equally confusing

// ❌ Any object vs primitive (goes through ToPrimitive)
[] == ""       // true ([]) → "" → equal)
[] == 0        // true ([] → "" → 0 → equal)
[1] == 1       // true ([1] → "1" → 1 → equal)

Why ESLint's eqeqeq Rule Exists

The eqeqeq rule (enforcing ===) exists because:

  1. Readability: === has obvious semantics; == requires knowing 12 rules by heart.
  2. Defensive programming: =='s type coercion can silently mask bugs when functions receive unexpected types.
  3. Maintenance cost: Code using == is prone to subtle bugs during refactoring.
// ESLint rule configuration
{
  "rules": {
    "eqeqeq": ["error", "always", { "null": "ignore" }]
    // "null": "ignore" permits the valid x == null pattern
  }
}

🔸 Level 2 · How It Actually Runs

Abstract Equality Comparison Decision Tree

IsLooselyEqual(x, y)
│
├─ x and y have the same type?
│   └─ YES → use strict equality (IsStrictlyEqual) directly
│             (includes NaN !== NaN, +0 === -0, etc.)
│
└─ NO (different types):
    │
    ├─ x is null, y is undefined? → true (hardcoded in spec)
    ├─ x is undefined, y is null? → true (hardcoded in spec)
    │
    ├─ x or y is Number, the other is String?
    │   └─ Convert String to Number, recurse IsLooselyEqual
    │       "42" == 42  →  42 == 42  →  true
    │       "abc" == 1  →  NaN == 1  →  false
    │
    ├─ x or y is BigInt, the other is String?
    │   └─ Try to convert String to BigInt; if fails, return false
    │
    ├─ x or y is Boolean?
    │   └─ Convert Boolean to Number (true→1, false→0), recurse
    │       ⚠️ This is the biggest trap! Boolean becomes number first,
    │          not compared as truthy/falsy against the other operand
    │       false == ""  →  0 == ""  →  0 == 0  →  true
    │
    ├─ x or y is Object, the other is String/Number/BigInt/Symbol?
    │   └─ Call ToPrimitive on the Object, recurse IsLooselyEqual
    │       [] == ""  →  "" == ""  →  true
    │       [] == 0   →  "" == 0   →  0 == 0  →  true
    │
    ├─ x is BigInt, y is Number (or vice versa)?
    │   └─ Compare mathematical values (handle NaN/Infinity specially)
    │       1n == 1   →  true
    │       1n == 1.5 →  false
    │
    └─ All other cases → false

The Boolean Trap Illustrated

Deriving false == "0" (most commonly misunderstood):

Step 1: Boolean detected
  false == "0"
  └─ Rule: boolean → convert to number
  → 0 == "0"

Step 2: Number vs String detected
  0 == "0"
  └─ Rule: string → convert to number
  → 0 == 0

Step 3: Same type, use strict equality
  0 === 0 → true

Summary: false == "0" is true!

Why intuition fails:
  "0" is a non-empty string, intuitively truthy, so "truthy != false"
  But == does NOT compare truthy/falsy — it does numeric conversion and comparison.

🔺 Level 3 · How the Spec Defines It

7.2.13 IsLooselyEqual(x, y) — All 12 Rules

Spec text (ECMA-262, Section 7.2.13):

7.2.13 IsLooselyEqual ( x, y )

  1. If Type(x) is Type(y), then a. Return IsStrictlyEqual(x, y).
  2. If x is null and y is undefined, return true.
  3. If x is undefined and y is null, return true.
  4. NOTE: This step is replaced in section B.3.6.2.
  5. If x is a Number and y is a String, return ! IsLooselyEqual(x, ! ToNumber(y)).
  6. If x is a String and y is a Number, return ! IsLooselyEqual(! ToNumber(x), y).
  7. If x is a BigInt and y is a String, then a. Let n be StringToBigInt(y). b. If n is undefined, return false. c. Return ! IsLooselyEqual(x, n).
  8. If x is a String and y is a BigInt, return ! IsLooselyEqual(y, x).
  9. If x is a Boolean, return ! IsLooselyEqual(! ToNumber(x), y).
  10. If y is a Boolean, return ! IsLooselyEqual(x, ! ToNumber(y)).
  11. If x is either a String, a Number, a BigInt, or a Symbol and y is an Object, return ! IsLooselyEqual(x, ? ToPrimitive(y)).
  12. If x is an Object and y is either a String, a Number, a BigInt, or a Symbol, return ! IsLooselyEqual(? ToPrimitive(x), y).
  13. If x is a BigInt and y is a Number, or if x is a Number and y is a BigInt, then a. If x or y is NaN, +∞𝔽, or -∞𝔽, return false. b. If ℝ(x) = ℝ(y), return true; otherwise return false.
  14. Return false.

Rule-by-rule explanation:

Rule Trigger condition Action
1 Both sides same type Use strict equality (NaN!==NaN included)
2 x=null, y=undefined Hardcoded return true
3 x=undefined, y=null Hardcoded return true
5 x=Number, y=String Convert y to Number, recurse
6 x=String, y=Number Convert x to Number, recurse
7-8 BigInt and String Convert String to BigInt; if fails, false
9 x=Boolean Convert x to Number, recurse (not y)
10 y=Boolean Convert y to Number, recurse
11 y=Object, x is primitive ToPrimitive(y), recurse
12 x=Object, y is primitive ToPrimitive(x), recurse
13 BigInt and Number Compare mathematical values
14 Everything else false

The ! symbol in the spec: In spec algorithms, the ! prefix means "assert this operation will not produce an abrupt completion (i.e., will not throw)." It is NOT the JavaScript logical-not operator.


💎 Level 4 · Edge Cases and Traps

Trap 1: 5-Step Derivation of '' == false

Expression: '' == false

Types: String == Boolean

Step 1: Rule 10 — y (false) is Boolean
  → ToNumber(false) = 0
  → Recurse: '' == 0

Types: String == Number

Step 2: Rule 6 — x ('') is String, y (0) is Number
  → ToNumber('') = 0
  → Recurse: 0 == 0

Types: Number == Number

Step 3: Rule 1 — same type (both Number)
  → IsStrictlyEqual(0, 0)
  → 0 === 0 → true

Conclusion: '' == false is true

Why intuition fails:
  '' is falsy, false is falsy — both "false-y", so they seem equal
  But == doesn't compare truthy/falsy — it converts to numbers and compares

Trap 2: 6-Step Derivation of [] == false

Expression: [] == false

Types: Object == Boolean

Step 1: Rule 10 — y (false) is Boolean
  → ToNumber(false) = 0
  → Recurse: [] == 0

Types: Object == Number

Step 2: Rule 12 — x ([]) is Object, y (0) is Number
  → ToPrimitive([])
  → [].valueOf() → []  (object, not primitive, continue)
  → [].toString() → "" (primitive, return)
  → Recurse: "" == 0

Types: String == Number

Step 3: Rule 6 — x ("") is String, y (0) is Number
  → ToNumber("") = 0
  → Recurse: 0 == 0

Types: Number == Number

Step 4: Rule 1 — same type
  → 0 === 0 → true

Conclusion: [] == false is true
Note: two type conversions occurred (Object→String→Number)

Trap 3: Complete Derivation of [] == ![]

This is JavaScript's most notorious "weird" expression:

Expression: [] == ![]

Step 1: Evaluate ![] first (unary operator, higher precedence than ==)
  ![] = !ToBoolean([])
  ToBoolean([]) = true  (all objects are truthy, including empty arrays)
  !true = false
  So ![] = false

Expression becomes: [] == false

Step 2: Derive [] == false (same as Trap 2 above)
  → [] == 0   (boolean → number)
  → "" == 0   (ToPrimitive)
  → 0 == 0    (string → number)
  → true

Conclusion: [] == ![] is true

Root cause:
  [] is truthy (as boolean it's true)
  ![] is false (negating truthy gives false)
  But [] == false takes the numeric comparison path:
  [] converts to 0, false converts to 0, equal

Trap 4: Why null == 0 is False

Many developers mistakenly believe null participates in numeric comparison. In reality, null has its own exclusive rules:

Expression: null == 0

Types: Null == Number

Checking each rule:
  Rule 1: different types (Null != Number) → continue
  Rule 2: x=null, y=undefined? → y=0 is not undefined → continue
  Rule 3: x=undefined? → x=null is not undefined → continue
  Rule 5: x=Number, y=String? → no → continue
  Rule 6: x=String, y=Number? → no → continue
  Rule 9: x=Boolean? → no → continue
  Rule 10: y=Boolean? → no → continue
  Rule 11: y=Object? → y=0 is not Object → continue
  Rule 12: x=Object? → x=null is not Object → continue
  Rule 13: BigInt? → no → continue
  Rule 14: return false

Conclusion: null == 0 is false!

null only equals undefined (rules 2 & 3).
It does not equal any other value, including
0, "", and false (all falsy values).
// Complete summary of null equality
null == null        // true (rule 1: same type, strict equality)
null == undefined   // true (rule 2: hardcoded)
null == 0           // false
null == ""          // false
null == false       // false
null == "null"      // false
null == NaN         // false
null == []          // false (even though [] is falsy)

Trap 5: == Fails Transitivity

Mathematically, equality must satisfy transitivity: if a=b and b=c, then a=c. The == operator does not satisfy this property:

"0" == 0    // true  ("0" → number: 0, 0 == 0)
0  == ""    // true  ("" → number: 0, 0 == 0)
"0" == ""   // false! (same type, both strings, "0" !== "")

// Therefore:
// "0" == 0 is true
// 0  == "" is true
// But "0" != ""
// This is a failure of transitivity: a==b, b==c, but a!=c
// Where a="0", b=0, c=""

Verification:

console.log("0" == 0);   // true  ← "0" converts to number
console.log(0  == "");   // true  ← "" converts to number
console.log("0" == "");  // false ← both strings, direct comparison

// Transitivity failure: "0"==0 and 0=="" but "0"!=""

This is the fundamental reason eqeqeq exists — == doesn't form a mathematical equivalence relation, not merely a "coding style" concern.

Trap 6: Performance Cost of ==

// Micro-benchmark (Chrome 120, milliseconds per million operations)
// === : ~1ms  (no type conversion, direct bit comparison)
// == same type: ~1ms (actually follows the === path, no difference)
// == cross-type: ~3-8ms (depending on conversion complexity)

// For high-frequency code (e.g., rendering loops), use === rather than ==
// Not because === is "safer" — but because its semantics are obvious
// and its performance is predictable

Chapter Summary

  1. The only recommended use of == is x == null: leveraging the hardcoded rules 2/3 to simultaneously catch null and undefined — a feature explicitly supported by the spec. Pair with ESLint's { "null": "ignore" } option.

  2. Boolean involvement in == is the most dangerous case: rules 9/10 convert the boolean to a number (true→1, false→0) first, rather than converting the other operand to boolean. This makes false == "" true, but false == "false" false.

  3. null only equals undefined: null does not match any type-conversion rule (rules 5-13 all fail to match). Only rules 2/3 (the hardcoded mutual equality with undefined) produce true. null == 0 and null == "" are both false.

  4. [] == ![] is true because: ![] evaluates to false first (empty array is truthy), then [] == false converts through two steps, ultimately comparing 0 == 0. Each step is a necessary consequence of the spec.

  5. == does not satisfy transitivity: "0" == 0 (true) and 0 == "" (true), but "0" != "" (false). This violates the mathematical transitivity law of equivalence relations — and is the fundamental justification for the eqeqeq rule, not just a stylistic preference.

Rate this chapter
4.6  / 5  (53 ratings)

💬 Comments