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:
- Readability:
===has obvious semantics;==requires knowing 12 rules by heart. - Defensive programming:
=='s type coercion can silently mask bugs when functions receive unexpected types. - 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 )
- If Type(x) is Type(y), then a. Return IsStrictlyEqual(x, y).
- If x is null and y is undefined, return true.
- If x is undefined and y is null, return true.
- NOTE: This step is replaced in section B.3.6.2.
- If x is a Number and y is a String, return ! IsLooselyEqual(x, ! ToNumber(y)).
- If x is a String and y is a Number, return ! IsLooselyEqual(! ToNumber(x), y).
- 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).
- If x is a String and y is a BigInt, return ! IsLooselyEqual(y, x).
- If x is a Boolean, return ! IsLooselyEqual(! ToNumber(x), y).
- If y is a Boolean, return ! IsLooselyEqual(x, ! ToNumber(y)).
- If x is either a String, a Number, a BigInt, or a Symbol and y is an Object, return ! IsLooselyEqual(x, ? ToPrimitive(y)).
- If x is an Object and y is either a String, a Number, a BigInt, or a Symbol, return ! IsLooselyEqual(? ToPrimitive(x), y).
- 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.
- 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
-
The only recommended use of
==isx == 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. -
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 makesfalse == ""true, butfalse == "false"false. -
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 == 0andnull == ""are both false. -
[] == ![]is true because:![]evaluates tofalsefirst (empty array is truthy), then[] == falseconverts through two steps, ultimately comparing0 == 0. Each step is a necessary consequence of the spec. -
==does not satisfy transitivity:"0" == 0(true) and0 == ""(true), but"0" != ""(false). This violates the mathematical transitivity law of equivalence relations โ and is the fundamental justification for theeqeqeqrule, not just a stylistic preference.