JavaScript Interview Prep

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== ResultWhy
"5" == 5trueString coerced to Number
true == 1trueBoolean coerced to Number
null == undefinedtrueSpecial spec exception
[] == falsetrueBoth coerced β†’ 0 == 0
[] == ![]true[] β†’ 0, ![] β†’ 0
({}) == "[object Object]"trueObject coerced to String

Table of Contents


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):

  1. null / undefined β€” Equal to each other only
  2. Boolean β†’ Number β€” Booleans convert first
  3. String β†’ Number β€” When compared to Number
  4. 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):

  1. Try valueOf() β€” if primitive, return it
  2. Try toString() β€” if primitive, return it
  3. Throw TypeError

For β€œstring” hint:

  1. Try toString() β€” if primitive, return it
  2. Try valueOf() β€” if primitive, return it
  3. 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.

InputResult
undefinedNaN
null0
true1
false0
"" (empty string)0
" " (whitespace)0
"123"123
"12.3"12.3
"0x1A"26 (hex)
"abc"NaN
Symbol()TypeError
ObjectToPrimitive(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.

InputResult
undefined"undefined"
null"null"
true"true"
false"false"
NumberDecimal representation
-0"0" (hides negative)
Infinity"Infinity"
NaN"NaN"
SymbolTypeError
ObjectToPrimitive(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):

  • false
  • 0, -0, 0n
  • "" (empty string)
  • null
  • undefined
  • NaN

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:

  1. Boolean present β†’ false β†’ 0
  2. Now: [] == 0
  3. Object present β†’ [] β†’ "" (via toString)
  4. Now: "" == 0
  5. String vs Number β†’ "" β†’ 0
  6. Now: 0 == 0
  7. Return true

2. [] == ![] β†’ true

[] == ![]

Step-by-step:

  1. ![] evaluates first: [] is truthy β†’ ![] = false
  2. Now: [] == false
  3. Boolean present β†’ false β†’ 0
  4. Now: [] == 0
  5. Object present β†’ [] β†’ "" β†’ 0
  6. Now: 0 == 0
  7. 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:

  1. Boolean present β†’ false β†’ 0
  2. Now: "0" == 0
  3. String vs Number β†’ "0" β†’ 0
  4. Now: 0 == 0
  5. 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:

  1. Object present β†’ {} calls toString() β†’ "[object Object]"
  2. Now: "[object Object]" == "[object Object]"
  3. Same type, same value
  4. 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:

  1. false β†’ 0 (Boolean to Number)
  2. [] == 0
  3. [] β†’ "" (ToPrimitive with toString)
  4. "" == 0
  5. "" β†’ 0 (String to Number)
  6. 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:

  1. Check for Symbol.toPrimitive method β€” if present, call it
  2. For β€œnumber” hint: try valueOf(), then toString()
  3. For β€œstring” hint: try toString(), then valueOf()
  4. 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
  1. true β†’ 1 (Boolean to Number)
  2. [1] == 1
  3. [1] β†’ "1" (ToPrimitive β†’ toString)
  4. "1" == 1
  5. "1" β†’ 1 (String to Number)
  6. 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 == null pattern and when it’s acceptable
  • Understands ToPrimitive with valueOf/toString chain
  • Can explain why === is the default recommendation

Further Reading