double equal == vs triple equal === in JavaScript: Abstract Equality and Type Coercion
π― Interview-Ready Deep Dive β The difference between double equal(==) and triple equal(===) is one of the most common JavaScript interview questions but most answers stop at use === to avoid coercion. This guide goes deeper understanding how coercion works, when it's intentional, and why the spec designed it this way.
== vs === in JavaScript: Abstract Equality and Type Coercion
The difference between == and === is one of the most common JavaScript interview questions β but most answers stop at βuse === to avoid coercion.β This guide goes deeper: understanding how coercion works, when itβs intentional, and why the spec designed it this way.
TL;DR:
===compares values without type conversion.==converts types first, then uses===. The==operator follows a specific coercion priority defined in the ECMAScript specification. Learn the rules, and youβll predict any comparison result.
Quick Reference
| Comparison | == Result | Why |
|---|---|---|
"5" == 5 | true | String coerced to Number |
true == 1 | true | Boolean coerced to Number |
null == undefined | true | Special spec exception |
[] == false | true | Both coerced β 0 == 0 |
[] == ![] | true | [] β 0, ![] β 0 |
({}) == "[object Object]" | true | Object coerced to String |
Table of Contents
- The Relationship Between == and ===
- Abstract Equality Algorithm
- Type Coercion Algorithms
- The Coercion Priority Chain
- Common Gotchas Explained
- When == Is Intentional
- Why === Is the Default
- Interview Q&A
- Interview Self-Check
The Relationship Between == and ===
Key insight: == is NOT a separate equality algorithm. Itβs coercion + Strict Equality.
flowchart TD
START["x == y"] --> CHECK{"Same type?"}
CHECK -->|Yes| STRICT["Return x === y"]
CHECK -->|No| COERCE["Coerce types"]
COERCE --> RECURSE["Recurse with coerced values"]
RECURSE --> CHECK
style STRICT fill:#4ade80,stroke:#166534,color:#000
style COERCE fill:#fbbf24,stroke:#b45309,color:#000
When types match, == behaves identically to ===:
// Same type β no coercion, == behaves like ===
5 == 5; // true (same as ===)
"hello" == "hello"; // true (same as ===)
NaN == NaN; // false (same as ===)
When types differ, == coerces before comparing:
// Different types β coercion happens
"5" == 5; // true (string "5" β number 5)
true == 1; // true (boolean true β number 1)
Abstract Equality Algorithm
The == operator follows this algorithm (simplified):
AbstractEqualityComparison(x, y):
1. If Type(x) === Type(y):
β Return Strict Equality(x, y)
2. If x is null and y is undefined (or vice versa):
β Return true
3. If one is Number and one is String:
β Coerce String to Number, compare
4. If one is BigInt and one is String:
β Coerce String to BigInt, compare
5. If one is Boolean:
β Coerce Boolean to Number, recurse
6. If one is Object and one is primitive:
β Coerce Object via ToPrimitive, recurse
7. If one is BigInt and one is Number:
β If Number is finite integer, compare values; otherwise return false
8. Return false
Coercion precedence (simplified mental model):
- null / undefined β Equal to each other only
- Boolean β Number β Booleans convert first
- String β Number β When compared to Number
- Object β Primitive β Via ToPrimitive
Type Coercion Algorithms
ToPrimitive
Converts an object to a primitive value. The algorithm depends on the preferred type hint.
For βnumberβ hint (default for most operations):
- Try
valueOf()β if primitive, return it - Try
toString()β if primitive, return it - Throw TypeError
For βstringβ hint:
- Try
toString()β if primitive, return it - Try
valueOf()β if primitive, return it - Throw TypeError
Symbol.toPrimitive (ES2015):
Objects can define custom coercion behavior:
const obj = {
[Symbol.toPrimitive](hint) {
if (hint === "number") return 42;
if (hint === "string") return "custom";
return null; // default hint
}
};
+obj; // 42 (number hint)
`${obj}`; // "custom" (string hint)
obj + ""; // "null" (default hint, returns null β "null")
ToNumber
Converts any value to a Number.
| Input | Result |
|---|---|
undefined | NaN |
null | 0 |
true | 1 |
false | 0 |
"" (empty string) | 0 |
" " (whitespace) | 0 |
"123" | 123 |
"12.3" | 12.3 |
"0x1A" | 26 (hex) |
"abc" | NaN |
Symbol() | TypeError |
| Object | ToPrimitive(obj, "number") β ToNumber |
Number(undefined); // NaN
Number(null); // 0
Number(true); // 1
Number(false); // 0
Number(""); // 0
Number(" 42 "); // 42 (trims whitespace)
Number("42abc"); // NaN
Number([]); // 0 ([] β "" β 0)
Number([1]); // 1 ([1] β "1" β 1)
Number([1,2]); // NaN ([1,2] β "1,2" β NaN)
ToString
Converts any value to a String.
| Input | Result |
|---|---|
undefined | "undefined" |
null | "null" |
true | "true" |
false | "false" |
| Number | Decimal representation |
-0 | "0" (hides negative) |
Infinity | "Infinity" |
NaN | "NaN" |
| Symbol | TypeError |
| Object | ToPrimitive(obj, "string") β ToString |
String(undefined); // "undefined"
String(null); // "null"
String(-0); // "0" β hides the sign!
String([1, 2, 3]); // "1,2,3"
String({}); // "[object Object]"
String({ toString: () => "custom" }); // "custom"
ToBoolean
Converts any value to a Boolean. No custom coercion β just a lookup table.
Falsy values (return false):
false0,-0,0n""(empty string)nullundefinedNaN
Everything else returns true, including:
"0"(non-empty string)"false"(non-empty string)[](empty array){}(empty object)
Boolean(""); // false
Boolean("0"); // true β non-empty string!
Boolean([]); // true β object!
Boolean({}); // true β object!
Boolean(new Boolean(false)); // true β object!
The Coercion Priority Chain
When == encounters different types, it follows this priority:
flowchart TD
START["x == y (different types)"] --> NULL{"null or undefined?"}
NULL -->|"null == undefined"| TRUE["Return true"]
NULL -->|No| BOOL{"Boolean present?"}
BOOL -->|Yes| TONUMBER["Convert Boolean β Number"]
TONUMBER --> RECURSE1["Recurse"]
BOOL -->|No| OBJ{"Object present?"}
OBJ -->|Yes| TOPRIM["Convert Object β ToPrimitive"]
TOPRIM --> RECURSE2["Recurse"]
OBJ -->|No| NUMSTR{"Number + String?"}
NUMSTR -->|Yes| STRTONUM["Convert String β Number"]
STRTONUM --> COMPARE["Compare with ==="]
NUMSTR -->|No| FALSE["Return false"]
style TRUE fill:#4ade80,stroke:#166534,color:#000
style FALSE fill:#f87171,stroke:#b91c1c,color:#000
style TONUMBER fill:#fbbf24,stroke:#b45309,color:#000
style TOPRIM fill:#fbbf24,stroke:#b45309,color:#000
style STRTONUM fill:#fbbf24,stroke:#b45309,color:#000
Common Gotchas Explained
1. [] == false β true
[] == false
Step-by-step:
- Boolean present β
falseβ0 - Now:
[] == 0 - Object present β
[]β""(via toString) - Now:
"" == 0 - String vs Number β
""β0 - Now:
0 == 0 - Return
true
2. [] == ![] β true
[] == ![]
Step-by-step:
![]evaluates first:[]is truthy β![]=false- Now:
[] == false - Boolean present β
falseβ0 - Now:
[] == 0 - Object present β
[]β""β0 - Now:
0 == 0 - Return
true
3. null == undefined β true
null == undefined // true
null === undefined // false
This is a special case in the spec. Itβs explicitly defined, not derived from coercion rules.
Why? Both represent βabsence of valueβ β comparing them loosely returns true.
Importantly:
null == 0; // false β null doesn't coerce to 0 in ==
null == ""; // false β null only equals undefined
null == false; // false β null only equals undefined
4. "0" == false β true
"0" == false
Step-by-step:
- Boolean present β
falseβ0 - Now:
"0" == 0 - String vs Number β
"0"β0 - Now:
0 == 0 - Return
true
But:
Boolean("0"); // true β "0" is truthy!
This is why == can be confusing β "0" is truthy but "0" == false is true.
5. {} == "[object Object]" β true
({}) == "[object Object]"
Step-by-step:
- Object present β
{}callstoString()β"[object Object]" - Now:
"[object Object]" == "[object Object]" - Same type, same value
- Return
true
When == Is Intentional
The x == null Pattern
This is the only widely-accepted use of ==:
// Checks both null AND undefined in one comparison
if (value == null) {
// value is null or undefined
}
// Equivalent to:
if (value === null || value === undefined) {
// value is null or undefined
}
Why this works: The spec explicitly makes null == undefined return true, and null doesnβt loosely equal anything else.
null == null; // true
null == undefined; // true
null == 0; // false
null == ""; // false
null == false; // false
Intentional String/Number Coercion
Occasionally, == is used for intentional coercion:
// User input is always a string
const input = document.getElementById("age").value;
// Intentional coercion
if (input == 18) {
// Works with "18" or 18
}
// But explicit is better:
if (Number(input) === 18) {
// Clear intent
}
Why === Is the Default
1. Predictability
=== has no implicit type conversion β what you see is what you get.
// With ===, the result is obvious
"5" === 5; // false β different types
// With ==, you need to know coercion rules
"5" == 5; // true β but why? (requires knowledge)
2. Performance
=== avoids coercion overhead and is easier for engines to optimize.
The real cost of == is not correctness β itβs cognitive load.
3. ESLint eqeqeq Rule
Most teams enable the eqeqeq rule:
{
"rules": {
"eqeqeq": ["error", "always", { "null": "ignore" }]
}
}
The "null": "ignore" allows the x == null pattern.
4. TypeScript Strictness
TypeScriptβs strict mode discourages == by flagging type mismatches at compile time.
Interview Q&A
Q: What's the difference between == and ===?
===(Strict Equality) compares values without type conversion. Different types are always unequal.==(Abstract Equality) performs type coercion first, then compares using Strict Equality.
5 === "5"; // false β different types
5 == "5"; // true β "5" coerced to 5
Q: Why does [] == false return true?
Coercion chain:
falseβ0(Boolean to Number)[] == 0[]β""(ToPrimitive with toString)"" == 0""β0(String to Number)0 == 0βtrue
Q: Why does null == undefined return true?
This is a special case defined in the ECMAScript specification. The spec explicitly states that null loosely equals undefined (and only undefined). This is intentional β both represent βabsence of value.β
null == undefined; // true (spec exception)
null == 0; // false (null doesn't coerce)
null == ""; // false
Q: When is it acceptable to use ==?
The x == null pattern is widely accepted:
if (value == null) {
// Checks both null and undefined
}
This is the only common case where == is preferred. ESLintβs eqeqeq rule typically allows this with { "null": "ignore" }.
Q: How does ToPrimitive work?
ToPrimitive converts objects to primitives:
- Check for
Symbol.toPrimitivemethod β if present, call it - For βnumberβ hint: try
valueOf(), thentoString() - For βstringβ hint: try
toString(), thenvalueOf() - Return the first primitive result
const obj = { valueOf: () => 42, toString: () => "hello" };
+obj; // 42 (number hint β valueOf)
`${obj}`; // "hello" (string hint β toString)
Interview Self-Check
1. Predict the coercion chain:
[1] == true
Reveal Answer
trueβ1(Boolean to Number)[1] == 1[1]β"1"(ToPrimitive β toString)"1" == 1"1"β1(String to Number)1 == 1βtrue
2. Why is this false?
[] == []
Reveal Answer
Same type (both objects), so no coercion. Objects compare by reference identity, not value. Two array literals create two different objects with different references.
3. What makes this different?
null == 0 // ?
undefined == 0 // ?
Reveal Answer
null == 0; // false β null only equals undefined
undefined == 0; // false β undefined only equals null
Unlike false or "", null and undefined do not coerce to numbers in ==. They only equal themselves and each other.
Summary Checklist
- Knows that == is coercion + Strict Equality
- Can recite the coercion priority: null/undefined β Boolean β Object β String/Number
- Understands why null == undefined is a special case
- Can trace through [] == false step by step
- Knows the
x == nullpattern and when itβs acceptable - Understands ToPrimitive with valueOf/toString chain
- Can explain why === is the default recommendation
Further Reading
- JavaScript Primitive Types β The 7 primitive types and their behaviors
- JavaScript Equality Algorithms β Deep dive into ===, SameValue, SameValueZero
- ECMAScript: Abstract Equality Comparison
- ECMAScript: ToPrimitive