Episode 1 — Fundamentals / 1.18 — Operators and Type System
1.18.g — Type Coercion
In one sentence: JavaScript automatically converts values between types during operations (implicit coercion), and while the language provides explicit converters like
Number(),String(), andBoolean(), understanding the hidden conversion rules — especially the+operator's dual behavior — is essential for avoiding the bizarre results that make JavaScript famous.
Navigation: ← 1.18.f — Truthy and Falsy Values · 1.18 Exercise Questions →
1. Implicit coercion (automatic conversion)
JavaScript silently converts types when an operator expects a different type than what it receives.
The + operator: string concatenation wins
When + has any string operand, it converts the other operand to a string and concatenates:
console.log("5" + 3); // "53" (number → string)
console.log(5 + "3"); // "53" (number → string)
console.log("5" + true); // "5true" (boolean → string)
console.log("5" + null); // "5null" (null → string)
console.log("5" + undefined); // "5undefined"
console.log("5" + [1, 2]); // "51,2" (array → string)
console.log("5" + {}); // "5[object Object]"
Other arithmetic operators: number conversion wins
-, *, /, % always try to convert to numbers:
console.log("10" - 3); // 7 (string → number)
console.log("10" * 2); // 20
console.log("10" / 5); // 2
console.log("10" % 3); // 1
console.log("5" - "2"); // 3 (both → numbers)
console.log(true + 1); // 2 (true → 1)
console.log(false + 1); // 1 (false → 0)
console.log(null + 1); // 1 (null → 0)
console.log(undefined + 1); // NaN (undefined → NaN)
The + asymmetry summarized
| Expression | + result | - result |
|---|---|---|
"5" op 3 | "53" (concat) | 2 (math) |
"5" op "3" | "53" (concat) | 2 (math) |
true op 1 | 2 (math, no string) | 0 (math) |
null op 1 | 1 (math, no string) | -1 (math) |
Key rule: + checks for strings first. All other arithmetic operators convert to numbers.
2. Evaluation order matters with +
console.log(1 + 2 + "3"); // "33"
// Step 1: 1 + 2 → 3 (both numbers)
// Step 2: 3 + "3" → "33" (string wins)
console.log("1" + 2 + 3); // "123"
// Step 1: "1" + 2 → "12" (string wins)
// Step 2: "12" + 3 → "123" (string wins)
console.log(1 + "2" + 3); // "123"
// Step 1: 1 + "2" → "12"
// Step 2: "12" + 3 → "123"
Once concatenation starts, it stays concatenation (left-to-right evaluation).
3. Explicit coercion: Number(), String(), Boolean()
Number(value) — convert to number
console.log(Number("42")); // 42
console.log(Number("42.5")); // 42.5
console.log(Number("")); // 0
console.log(Number(" ")); // 0 (whitespace → 0)
console.log(Number(" 42 ")); // 42 (trims whitespace)
console.log(Number("hello")); // NaN
console.log(Number("42abc")); // NaN (partial numbers fail)
console.log(Number(true)); // 1
console.log(Number(false)); // 0
console.log(Number(null)); // 0
console.log(Number(undefined)); // NaN
console.log(Number([])); // 0 ([] → "" → 0)
console.log(Number([5])); // 5 ([5] → "5" → 5)
console.log(Number([1, 2])); // NaN ([1,2] → "1,2" → NaN)
console.log(Number({})); // NaN
parseInt() and parseFloat()
Unlike Number(), these parse from the beginning of a string and stop at the first non-numeric character:
console.log(parseInt("42px")); // 42 (stops at "p")
console.log(parseInt("0xFF", 16)); // 255 (hexadecimal)
console.log(parseInt("111", 2)); // 7 (binary)
console.log(parseInt("")); // NaN (no digits)
console.log(parseInt("hello")); // NaN
console.log(parseFloat("3.14em")); // 3.14
console.log(parseFloat("0.5")); // 0.5
console.log(parseFloat(".5")); // 0.5
Always pass the radix to parseInt:
// Without radix — potential issues with older engines
parseInt("08"); // 8 (modern) — historically could be 0 (octal)
// With radix — always safe
parseInt("08", 10); // 8
Unary + operator for number conversion
The unary + is a shorthand for Number():
console.log(+"42"); // 42
console.log(+""); // 0
console.log(+true); // 1
console.log(+false); // 0
console.log(+null); // 0
console.log(+undefined); // NaN
console.log(+"hello"); // NaN
console.log(+[]); // 0
console.log(+[5]); // 5
Common pattern in practice:
const input = document.getElementById("age").value; // always a string
const age = +input; // convert to number
// Or: const age = Number(input);
String(value) — convert to string
console.log(String(42)); // "42"
console.log(String(0)); // "0"
console.log(String(true)); // "true"
console.log(String(false)); // "false"
console.log(String(null)); // "null"
console.log(String(undefined)); // "undefined"
console.log(String(NaN)); // "NaN"
console.log(String([])); // ""
console.log(String([1, 2, 3])); // "1,2,3"
console.log(String({})); // "[object Object]"
console.log(String(Symbol("x"))); // "Symbol(x)"
Template literals and .toString()
// Template literals call ToString internally
const num = 42;
console.log(`Value: ${num}`); // "Value: 42"
console.log(`${null}`); // "null"
console.log(`${undefined}`); // "undefined"
console.log(`${[1, 2]}`); // "1,2"
// .toString() method
console.log((42).toString()); // "42"
console.log((255).toString(16)); // "ff" (hexadecimal)
console.log((8).toString(2)); // "1000" (binary)
console.log(true.toString()); // "true"
// null.toString() → TypeError
// undefined.toString() → TypeError
Boolean(value) — convert to boolean
console.log(Boolean(0)); // false
console.log(Boolean("")); // false
console.log(Boolean(null)); // false
console.log(Boolean(undefined)); // false
console.log(Boolean(NaN)); // false
console.log(Boolean(1)); // true
console.log(Boolean("hello")); // true
console.log(Boolean([])); // true
console.log(Boolean({})); // true
See 1.18.f — Truthy and Falsy Values for the full breakdown.
4. Abstract operations: ToPrimitive, ToNumber, ToString (simplified)
The ECMAScript specification defines internal operations that power coercion. You do not call these directly, but understanding them explains every coercion result.
ToPrimitive
When an object needs to become a primitive (for +, comparison, etc.), JavaScript calls:
[Symbol.toPrimitive](hint)if definedvalueOf()— if it returns a primitive, use ittoString()— if it returns a primitive, use it- TypeError if neither returns a primitive
The hint is "number", "string", or "default" depending on the context.
// Default behavior for plain objects
const obj = {};
console.log(obj.valueOf()); // {} (returns itself — not a primitive)
console.log(obj.toString()); // "[object Object]" (primitive string)
// So ToPrimitive({}) → "[object Object]"
// Default behavior for arrays
const arr = [1, 2, 3];
console.log(arr.valueOf()); // [1, 2, 3] (returns itself)
console.log(arr.toString()); // "1,2,3" (primitive string)
// So ToPrimitive([1,2,3]) → "1,2,3"
// Custom ToPrimitive
const custom = {
[Symbol.toPrimitive](hint) {
if (hint === "number") return 42;
if (hint === "string") return "custom";
return "default";
}
};
console.log(+custom); // 42
console.log(`${custom}`); // "custom"
console.log(custom + ""); // "default"
ToNumber (simplified rules)
| Input | Output |
|---|---|
undefined | NaN |
null | 0 |
true | 1 |
false | 0 |
"" (empty string) | 0 |
" " (whitespace) | 0 |
"42" | 42 |
"42abc" | NaN |
| Object/Array | ToPrimitive first, then ToNumber |
ToString (simplified rules)
| Input | Output |
|---|---|
undefined | "undefined" |
null | "null" |
true | "true" |
false | "false" |
| Number | String representation ("42", "NaN", "Infinity") |
| Object | ToPrimitive (string hint), then ToString |
5. Comparison coercion rules
The == operator uses its own coercion rules (see 1.18.b):
console.log(5 == "5"); // true (string → number)
console.log(0 == false); // true (boolean → number)
console.log("" == false); // true (both → 0)
console.log(null == undefined); // true (special rule)
Relational operators (<, >, <=, >=) convert to numbers (or compare strings lexicographically if both are strings):
console.log("10" > "9"); // false (string comparison: "1" < "9")
console.log("10" > 9); // true (string → number: 10 > 9)
6. The infamous [] + [], [] + {}, {} + []
These are classic interview questions that test your understanding of ToPrimitive:
[] + []
console.log([] + []); // ""
// Step 1: [] → ToPrimitive → [].toString() → ""
// Step 2: [] → ToPrimitive → [].toString() → ""
// Step 3: "" + "" → ""
[] + {}
console.log([] + {}); // "[object Object]"
// Step 1: [] → "" (as above)
// Step 2: {} → ToPrimitive → {}.toString() → "[object Object]"
// Step 3: "" + "[object Object]" → "[object Object]"
{} + [] (context matters!)
// In the console, {} at the start is parsed as an empty block, not an object
// So {} + [] becomes: +[] → +"" → 0
// Console result: 0
// But wrapped in an expression:
console.log({} + []); // "[object Object]" (same as [] + {})
// To force object interpretation:
console.log(({}) + []); // "[object Object]"
More weird additions
console.log([] + null); // "null" ([] → "", ""+null → "null")
console.log([] + undefined); // "undefined"
console.log([] + true); // "true"
console.log({} + {}); // "[object Object][object Object]"
console.log([] - []); // 0 ([] → "" → 0, 0 - 0 = 0)
console.log({} - {}); // NaN ("[object Object]" → NaN)
7. Coercion in common operators and contexts
if / while / ternary — ToBoolean
if ("hello") { ... } // string → true
if (0) { ... } // number → false
while (arr.length) { ... } // number → boolean
const x = val ? "yes" : "no"; // val → boolean
Template literals — ToString
const obj = { name: "Alice" };
console.log(`User: ${obj}`); // "User: [object Object]"
// obj → ToPrimitive(string hint) → toString() → "[object Object]"
// Custom toString makes templates useful
const user = {
name: "Alice",
toString() { return this.name; }
};
console.log(`User: ${user}`); // "User: Alice"
Property keys — ToString
const obj = {};
obj[1] = "one"; // key 1 → "1"
obj[true] = "yes"; // key true → "true"
obj[null] = "nope"; // key null → "null"
console.log(obj["1"]); // "one"
console.log(obj["true"]); // "yes"
console.log(obj["null"]); // "nope"
console.log(Object.keys(obj)); // ["1", "true", "null"] — all strings
8. The unary + and - operators
// Unary + converts to number
console.log(+"3"); // 3
console.log(+true); // 1
console.log(+false); // 0
console.log(+null); // 0
console.log(+undefined); // NaN
console.log(+""); // 0
console.log(+"abc"); // NaN
console.log(+[]); // 0 ([] → "" → 0)
console.log(+[5]); // 5 ([5] → "5" → 5)
// Unary - converts to number and negates
console.log(-"3"); // -3
console.log(-true); // -1
console.log(-false); // -0
console.log(-[]); // -0
9. Common coercion patterns and anti-patterns
Converting user input (explicit — good)
const ageStr = prompt("Enter your age:");
const age = Number(ageStr); // explicit
// or: const age = parseInt(ageStr, 10);
// or: const age = +ageStr;
if (Number.isNaN(age)) {
console.log("Invalid age");
}
String-to-number gotchas
// These all silently produce unexpected results
Number(""); // 0 (not NaN!)
Number(" "); // 0
Number(null); // 0
Number(false); // 0
Number([]); // 0
Number([0]); // 0
// Use a validation wrapper
function strictParseNumber(str) {
if (typeof str !== "string" || str.trim() === "") return NaN;
return Number(str);
}
Avoiding implicit coercion (best practices)
// BAD: relies on implicit coercion
const total = "5" - 0; // works but unclear
const str = 42 + ""; // works but unclear
const bool = !!value; // acceptable idiom
// GOOD: explicit conversion
const total = Number("5");
const str = String(42);
const bool = Boolean(value);
// BAD: + used ambiguously
const result = input + 5; // concat or add? depends on input type
// GOOD: convert first
const result = Number(input) + 5; // always addition
Type-safe comparison
// BAD: relies on == coercion
if (value == 42) { ... }
// GOOD: explicit
if (Number(value) === 42) { ... }
// Or: if (value === 42) { ... } — if you know the type
10. Coercion summary table
| From → To | To Number | To String | To Boolean |
|---|---|---|---|
undefined | NaN | "undefined" | false |
null | 0 | "null" | false |
true | 1 | "true" | — |
false | 0 | "false" | — |
0 | — | "0" | false |
1 | — | "1" | true |
NaN | — | "NaN" | false |
"" | 0 | — | false |
"0" | 0 | — | true |
"42" | 42 | — | true |
"hello" | NaN | — | true |
[] | 0 | "" | true |
[5] | 5 | "5" | true |
[1,2] | NaN | "1,2" | true |
{} | NaN | "[object Object]" | true |
Key takeaways
+with a string triggers concatenation; all other arithmetic operators trigger numeric conversion.- Evaluation order matters:
1 + 2 + "3"is"33", not"123". Number()is strict (full string must be a number);parseInt()/parseFloat()are lenient (parse prefix).- The unary
+is shorthand forNumber()— common but less readable. - ToPrimitive calls
valueOf()thentoString()on objects — this explains[] + [] === ""and[] + {} === "[object Object]". Number("")is0, notNaN— a common source of validation bugs.- Always prefer explicit coercion (
Number(),String(),Boolean()) over relying on implicit conversions. - Property keys are always coerced to strings (or symbols).
Explain-It Challenge
Explain without notes:
- What is the result of
1 + 2 + "3" + 4? Walk through each step. - Why does
"5" + 3give"53"but"5" - 3gives2? - What is the difference between
Number("42px")andparseInt("42px")? - Explain why
[] + []results in"". - Why is explicit coercion (
Number(x)) preferred over implicit (x - 0)?
Navigation: ← 1.18.f — Truthy and Falsy Values · 1.18 Exercise Questions →