Chapter 3.7☕ 20 min read

Type Coercion: ToPrimitive, ToNumber, ToBoolean

[] + {} = '[object Object]' — magic nahi, spec hai. Step by step algorithm chalao bhai.

0101 — ToPrimitive: The Core Coercion Algorithm

When JavaScript encounters an object where it expects a primitive — during +, -, comparisons, or template literals — it runs an internal algorithm called ToPrimitive. This algorithm decides how to convert any object into a primitive value.

ToPrimitive takes a hint: 'number', 'string', or 'default' (which behaves like 'number' for most built-in objects). The hint determines the ORDER of method calls:

number hint (arithmetic, <, >): call valueOf() first → if primitive, use it → else call toString() → if primitive, use it → else TypeError

string hint (template literals, String()): call toString() first → if primitive, use it → else call valueOf() → if primitive, use it → else TypeError

const obj = {
  valueOf()  { return 42; },
  toString() { return 'hello'; }
};

// 'number' hint: valueOf() first
console.log(obj + 0);          // 42 — valueOf() → 42
console.log(obj - 0);          // 42 — arithmetic: number hint
console.log(obj * 2);          // 84 — arithmetic: number hint

// 'string' hint: toString() first
console.log(`${obj}`);         // 'hello' — template: string hint
console.log(String(obj));      // 'hello' — String(): string hint

// 'default' hint (behaves like number for most objects)
console.log(obj == 42);        // true — default hint → valueOf → 42

// If valueOf returns non-primitive:
const obj2 = {
  valueOf()  { return {}; },   // returns object — not a primitive
  toString() { return 'text'; }
};
console.log(obj2 + '');        // 'text' — valueOf fails, falls to toString
The ToPrimitive algorithm is defined in ECMAScript section 7.1.1. V8 implements it as C++ functions called ToPrimitive_Number and ToPrimitive_String. Every + operation that involves an object goes through this — it's called before any arithmetic or concatenation. Symbol.toPrimitive bypasses the valueOf/toString lookup entirely.
0202 — ToNumber: The Number Conversion Rules

ToNumber is called whenever a value must become a number: arithmetic -, *, /, unary +, and Number(). The rules are explicit in the ECMAScript spec:

// ToNumber rules:
console.log(Number(undefined));  // NaN
console.log(Number(null));       // 0
console.log(Number(true));       // 1
console.log(Number(false));      // 0
console.log(Number(''));         // 0 — empty string!
console.log(Number('42'));       // 42
console.log(Number('  42  '));   // 42 — whitespace trimmed!
console.log(Number('0x1F'));     // 31 — hex!
console.log(Number('0b1010'));   // 10 — binary!
console.log(Number('hello'));    // NaN
console.log(Number('42abc'));    // NaN — not a valid number

// Objects go through ToPrimitive first:
console.log(Number([]));         // 0  — [] → '' → 0
console.log(Number([42]));       // 42 — [42] → '42' → 42
console.log(Number([1, 2]));     // NaN — [1,2] → '1,2' → NaN
console.log(Number({}));         // NaN — {} → '[object Object]' → NaN

// Unary + is shorthand for Number():
console.log(+'42');              // 42
console.log(+true);              // 1
console.log(+null);              // 0
console.log(+undefined);         // NaN
console.log(+[]);                // 0
console.log(+{});                // NaN
⚠️ Number('') === 0 — one of the most confusing coercion rules. An empty string converts to 0, not NaN. This trips up validation code — checking if a value is 'a number' by wrapping it in Number() lets empty strings through as 0. Use Number.isFinite(Number(x)) AND check for empty string separately, or parse with parseInt/parseFloat.
0303 — ToBoolean: The 7 Falsy Values

ToBoolean is the simplest coercion algorithm: only 8 values are falsy, EVERYTHING else is truthy. No exceptions, no special cases, no hints.

The 8 falsy values — memorize these:

console.log(Boolean(false));     // false
console.log(Boolean(0));         // false
console.log(Boolean(-0));        // false
console.log(Boolean(0n));        // false — BigInt zero
console.log(Boolean(''));        // false — empty string only!
console.log(Boolean(null));      // false
console.log(Boolean(undefined)); // false
console.log(Boolean(NaN));       // false

// EVERYTHING else is truthy — including these surprises:
console.log(Boolean('0'));           // true — non-empty string!
console.log(Boolean('false'));       // true — non-empty string!
console.log(Boolean([]));            // true — empty array!
console.log(Boolean({}));            // true — empty object!
console.log(Boolean(function(){}));  // true — function!
console.log(Boolean(Infinity));      // true — infinity!
console.log(Boolean(-Infinity));     // true — negative infinity!
console.log(Boolean(new Boolean(false))); // true — object wrapper!

// !! as ToBoolean cast:
console.log(!!0);       // false
console.log(!!'');      // false
console.log(!!'hello'); // true
console.log(!!null);    // false
console.log(!![]);      // true — empty array is truthy!
ToBoolean is the ONLY coercion algorithm with no exceptions for 'hint'. It's purely a lookup: is the value in the falsy list? No → true. Yes → false. V8 compiles if(x) into a ToBoolean check on x — for SMIs (Small Integers), it's just checking if the value is zero. For HeapObjects, it checks the instance type against the falsy singletons.
0404 — The + Operator: Coercion's Most Complex Case

The + operator is BOTH addition AND string concatenation. The spec decides which based on operand types — and this makes it coercion's most complex case.

Algorithm: ToPrimitive('default') on both operands → if EITHER result is a string → concatenate → else → add as numbers

// [] + [] :
// ToPrimitive([]) → '' (array toString = join, empty = '')
// Both primitives: '' is string → concatenate
console.log([] + []);            // '' (empty string)

// [] + {} :
// ToPrimitive([]) → ''
// ToPrimitive({}) → '[object Object]'
// Either is string → concatenate
console.log([] + {});            // '[object Object]'

// {} + [] — STATEMENT POSITION:
// {} is parsed as EMPTY BLOCK (not object literal!)
// then +[] is unary plus: +'' = 0
// To make {} an object: wrap in parentheses
console.log(({}) + []);          // '[object Object]'

// Other coercion surprises:
console.log(1 + '2');            // '12' — number coerced to string
console.log('3' - 1);            // 2 — string coerced to number (- not ambiguous)
console.log('3' * '2');          // 6 — both to number
console.log(true + true);        // 2 — both to number (1+1)
console.log(true + '1');         // 'true1' — string concat
console.log(null + 1);           // 1 — null → 0
console.log(undefined + 1);      // NaN — undefined → NaN
console.log([] + 1);             // '1' — [] → '' → string concat!
The {} + [] = 0 vs ({}) + [] = '[object Object]' distinction is pure JavaScript parser behavior. When {} appears at the start of a statement, the parser assumes it's a block (like an if body). When it appears as an expression (after (, return, =, etc.) it's an object literal. This context-sensitivity is one of JS's most notorious parsing quirks.
0505 — == Loose Equality: The Full Algorithm

The == operator (Abstract Equality Comparison) has 10+ rules in the ECMAScript spec. It's the most complex coercion algorithm — and the most dangerous.

Key rules (simplified):

// null/undefined special case — only equal to each other
console.log(null == undefined);  // true
console.log(null == 0);          // false — null only equals null/undefined
console.log(null == '');         // false
console.log(null == false);      // false

// number vs string: string → number
console.log(42 == '42');         // true — '42' → 42
console.log(42 == '42.0');       // true — '42.0' → 42
console.log(0 == '');            // true — '' → 0
console.log(0 == '0');           // true — '0' → 0
console.log('' == '0');          // false — same type, different value!

// boolean: boolean → number FIRST
console.log(true == 1);          // true — true → 1
console.log(false == 0);         // true — false → 0
console.log(true == '1');        // true — true→1, '1'→1
console.log(false == '');        // true — false→0, ''→0

// The infamous [] == false:
// Step 1: boolean == object → convert boolean: false → 0
// Step 2: number == object → ToPrimitive([]) → ''
// Step 3: number == string → Number('') → 0
// Step 4: 0 == 0 → true!
console.log([] == false);        // true — four-step coercion!
console.log([] == 0);            // true
console.log([] == '');           // true
console.log([1] == 1);           // true — [1] → '1' → 1
⚠️ Golden Rule for ==: The == algorithm has 10+ steps and produces results that surprise even experienced developers. The only safe rules to remember: null == undefined (and nothing else), then forget everything else and use ===. ESLint's eqeqeq rule enforces this in production code. The one useful case for == in real code: x == null to check for null OR undefined in one shot.

Lo kar liya — Key Points:

  • ✅ ToPrimitive('number'): valueOf() first, then toString(). ToPrimitive('string'): toString() first, then valueOf()
  • ✅ ToNumber rules: null→0, undefined→NaN, ''→0, '42'→42, []→0, {}→NaN
  • ✅ ToBoolean: only 8 values are falsy. Everything else is truthy — including '0', [], {}, Infinity
  • ✅ + coerces with ToPrimitive('default') — if either result is string, concatenates. Otherwise adds numbers
  • ✅ {} + [] is 0 (statement position). ({}) + [] is '[object Object]' (expression position)
  • ✅ Loose == is a 10-step algorithm — avoid it everywhere except x == null (checks null AND undefined)
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase