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