A complete guide to understanding how JavaScript silently converts types — and how to master it.
- What is Type Coercion?
- JavaScript's Type System
- Implicit vs Explicit Coercion
- String Coercion
- Number Coercion
- Boolean Coercion
- The
+Operator — The Great Deceiver - Loose Equality (
==) vs Strict Equality (===) - Abstract Equality Comparison Algorithm
- Object-to-Primitive Coercion
- Relational Operators (
<,>,<=,>=) - Logical Operators and Short-Circuit Evaluation
- Nullish Coalescing (
??) - Template Literals and Coercion
- Array Coercion Edge Cases
- Symbol and BigInt Coercion
- Common Gotchas and Bugs
- Best Practices
- Quick Reference Cheat Sheet
Type coercion is the automatic or implicit conversion of values from one data type to another. JavaScript is a dynamically typed language, meaning variables don't have fixed types — and the engine will happily convert types behind the scenes when operations require it.
// You wrote this:
"5" + 3
// JavaScript did this silently:
"5" + String(3) // → "53"This behavior can be a superpower when understood — or a source of notorious bugs when not.
JavaScript has 8 data types:
| Type | Example | Primitive? |
|---|---|---|
undefined |
undefined |
✅ Yes |
null |
null |
✅ Yes |
boolean |
true, false |
✅ Yes |
number |
42, 3.14, NaN, Infinity |
✅ Yes |
bigint |
9007199254740991n |
✅ Yes |
string |
"hello" |
✅ Yes |
symbol |
Symbol("id") |
✅ Yes |
object |
{}, [], null |
❌ No |
Note:
typeof null === "object"is a historical JavaScript bug —nullis actually a primitive.
typeof undefined // "undefined"
typeof true // "boolean"
typeof 42 // "number"
typeof "hello" // "string"
typeof Symbol() // "symbol"
typeof 42n // "bigint"
typeof {} // "object"
typeof [] // "object"
typeof null // "object" ← the famous bug
typeof function(){} // "function"You intentionally convert a type using built-in functions.
// String conversion
String(42) // "42"
String(true) // "true"
String(null) // "null"
String(undefined) // "undefined"
String([1, 2, 3]) // "1,2,3"
// Number conversion
Number("42") // 42
Number("") // 0
Number(true) // 1
Number(false) // 0
Number(null) // 0
Number(undefined) // NaN
Number("hello") // NaN
// Boolean conversion
Boolean(0) // false
Boolean("") // false
Boolean(null) // false
Boolean(undefined)// false
Boolean(NaN) // false
Boolean("hello") // true
Boolean(42) // true
Boolean([]) // true ← empty array is truthy!
Boolean({}) // true ← empty object is truthy!JavaScript converts types automatically based on context.
// Implicit: JS engine converts for you
"5" * 2 // 10 (string → number)
"5" - 2 // 3 (string → number)
true + true // 2 (boolean → number)
false + 1 // 1 (boolean → number)
null + 1 // 1 (null → 0)
undefined + 1 // NaN (undefined → NaN)- Using the
+operator when one operand is a string - Using template literals
`${}` - Calling
.toString()on any value - Passing to
String()
// Explicit
String(42) // "42"
String(null) // "null"
String(undefined) // "undefined"
String(true) // "true"
String(false) // "false"
String([1, 2, 3]) // "1,2,3"
String({}) // "[object Object]"
String(Symbol("s")) // "Symbol(s)"
// Implicit via +
42 + "" // "42"
null + "" // "null"
undefined + "" // "undefined"
true + "" // "true"
[1, 2] + "" // "1,2"
{} + "" // "[object Object]"(42).toString() // "42"
(42).toString(2) // "101010" — binary!
(42).toString(16) // "2a" — hex!
(255).toString(16) // "ff"
true.toString() // "true"
[1, 2, 3].toString() // "1,2,3"
({}).toString() // "[object Object]"- Arithmetic operators:
-,*,/,%,** - Unary
+operator - Comparison operators with non-string operands
Number(),parseInt(),parseFloat()
// Explicit
Number("3.14") // 3.14
Number(" 42 ") // 42 — trims whitespace!
Number("") // 0
Number("0x1F") // 31 — hex string
Number("0b101") // 5 — binary string
Number("0o7") // 7 — octal string
Number(true) // 1
Number(false) // 0
Number(null) // 0
Number(undefined) // NaN
Number([]) // 0 — empty array!
Number([1]) // 1 — single-element array!
Number([1, 2]) // NaN — multi-element array
Number({}) // NaN
Number("abc") // NaN
// Unary + (implicit Number())
+"42" // 42
+"" // 0
+true // 1
+false // 0
+null // 0
+undefined // NaN
+[] // 0
+[1] // 1
+{} // NaN// parseInt reads until it hits a non-digit
parseInt("42px") // 42 ← stops at "p"
parseInt("3.14") // 3 ← stops at "."
parseInt("0xFF", 16) // 255 ← parse hex
parseInt("10", 2) // 2 ← parse binary
parseInt("abc") // NaN
parseInt("") // NaN ← unlike Number("") = 0
// parseFloat
parseFloat("3.14rem") // 3.14
parseFloat("3.14.15") // 3.14 ← stops at second dot
// Number is strict
Number("42px") // NaN ← can't parse "px"
Number("3.14") // 3.14
Number("") // 0 ← unlike parseInt("") = NaN// These are ALL the falsy values:
Boolean(false) // false
Boolean(0) // false
Boolean(-0) // false
Boolean(0n) // false ← BigInt zero
Boolean("") // false
Boolean(null) // false
Boolean(undefined) // false
Boolean(NaN) // false
// EVERYTHING else is truthy!
Boolean("false") // true ← non-empty string
Boolean("0") // true ← non-empty string
Boolean([]) // true ← empty array!
Boolean({}) // true ← empty object!
Boolean(function(){}) // true
Boolean(-1) // true
Boolean(Infinity) // true// if / else
if ("") { } else { console.log("falsy") } // "falsy"
if ([]) { console.log("truthy") } // "truthy" — gotcha!
// Logical operators
!!"hello" // true
!!0 // false
!!null // false
!![] // true ← empty array is truthy!
// Ternary
"" ? "yes" : "no" // "no"
[] ? "yes" : "no" // "yes" ← truthy!
// while / for conditions
let arr = [1, 2, 3]
while (arr.length) { // coerces arr.length (number) to boolean
arr.pop()
}The + operator has dual behavior: addition (numbers) and concatenation (strings). This makes it the most confusing operator in JavaScript.
If either operand is a string (or becomes a string after
toPrimitive),+concatenates. Otherwise, it adds.
// Number + Number = addition
1 + 2 // 3
// String + anything = concatenation
"1" + 2 // "12"
1 + "2" // "12"
"" + 1 // "1"
"" + true // "true"
"" + null // "null"
"" + undefined // "undefined"
// Watch out for left-to-right evaluation
1 + 2 + "3" // "33" (1+2=3, then 3+"3"="33")
"1" + 2 + 3 // "123" ("1"+2="12", then "12"+3="123")
// null and undefined
null + null // 0 (null→0, 0+0=0)
null + undefined // NaN (undefined→NaN)
undefined + undefined // NaN
null + 1 // 1 (null→0)
undefined + 1 // NaN
// boolean
true + true // 2 (1+1)
true + false // 1 (1+0)
true + 1 // 2
false + [] // "false" — [] converts to "", false→"false"
// Objects and Arrays
[] + [] // "" ([]→"", ""+"" = "")
[] + {} // "[object Object]"
{} + [] // 0 ← when {} is a block, not object!
({}) + [] // "[object Object]"// In a script / as a statement:
{} + [] // 0
// {} is parsed as an EMPTY BLOCK, not an object literal
// So it becomes: {} (block); +[] (unary plus on array)
// +[] → +("") → 0
// When forced to be an expression:
({}) + [] // "[object Object]"
var x = {} + [] // "[object Object]"
console.log({} + []) // "[object Object]" ← expression context// === never coerces. Different types = false.
1 === 1 // true
1 === "1" // false
null === undefined // false
NaN === NaN // false ← NaN is never equal to itself!
// Use Object.is() for NaN comparison
Object.is(NaN, NaN) // true
Object.is(-0, 0) // false ← also detects -0 vs 0// == coerces types before comparing
1 == "1" // true (string→number)
1 == true // true (true→1)
0 == false // true (false→0)
0 == "" // true (""→0)
0 == "0" // true ("0"→0)
"" == false // true (both→0)
null == undefined // true (special rule)
null == 0 // false (null only == null/undefined)
null == false // false
undefined == false // false
NaN == NaN // false (NaN is never equal)
// Arrays and objects
[] == false // true ([]→""→0, false→0)
[] == 0 // true ([]→""→0)
[] == "" // true ([]→"")
[1] == 1 // true ([1]→"1"→1)
[[]] == 0 // true ([[]]→[]→""→0)// Confusing truths:
"" == false // true
"" == 0 // true
false == 0 // true
// But:
"" != false != 0 // they're not all equal to each other
// wait — actually:
"" == false == 0 // this is parsed as ("" == false) == 0
// → true == 0 → 1 == 0 → false!Here's the actual algorithm JavaScript uses for x == y:
1. If Type(x) === Type(y):
- If both are NaN → false
- Otherwise, compare values
2. If x is null and y is undefined → true
3. If x is undefined and y is null → true
4. If Type(x) is Number and Type(y) is String:
→ compare x == ToNumber(y)
5. If Type(x) is String and Type(y) is Number:
→ compare ToNumber(x) == y
6. If Type(x) is Boolean:
→ compare ToNumber(x) == y
7. If Type(y) is Boolean:
→ compare x == ToNumber(y)
8. If Type(x) is String/Number/Symbol and Type(y) is Object:
→ compare x == ToPrimitive(y)
9. If Type(x) is Object and Type(y) is String/Number/Symbol:
→ compare ToPrimitive(x) == y
10. Otherwise → false
// Tracing through: [] == false
// Step 6: false is Boolean → [] == ToNumber(false) → [] == 0
// Step 9: [] is Object → ToPrimitive([]) == 0 → "" == 0
// Step 5: "" is String → ToNumber("") == 0 → 0 == 0 → true ✅
// Tracing: null == false
// Step 2/3: null is not undefined → not matched
// Step 6: false is Boolean → null == ToNumber(false) → null == 0
// Step 10: types don't match remaining rules → false ✅When an object needs to become a primitive, JavaScript uses the ToPrimitive abstract operation. It looks for these methods in order:
// "number" hint → prefers valueOf() first
// "string" hint → prefers toString() first
// "default" hint → same as "number" for most objectsconst obj = {
valueOf() { return 42 },
toString() { return "hello" }
}
+obj // 42 (number hint → valueOf first)
`${obj}` // "hello" (string hint → toString first... wait)
obj + "" // "42" (default hint → valueOf first → 42 → "42")
obj + 1 // 43 (number: valueOf → 42 + 1)The most powerful override — lets you control coercion for all hints:
const temperature = {
celsius: 25,
[Symbol.toPrimitive](hint) {
if (hint === "number") return this.celsius
if (hint === "string") return `${this.celsius}°C`
return this.celsius // "default"
}
}
+temperature // 25 (number hint)
`${temperature}` // "25°C" (string hint)
temperature + 0 // 25 (default hint → number)
temperature + "" // "25" (default hint → number → string)
console.log(`Temp: ${temperature}`) // "Temp: 25°C"class Money {
constructor(amount, currency) {
this.amount = amount
this.currency = currency
}
valueOf() {
return this.amount // Used in arithmetic
}
toString() {
return `${this.amount} ${this.currency}` // Used in string context
}
[Symbol.toPrimitive](hint) {
if (hint === "string") return this.toString()
return this.valueOf()
}
}
const price = new Money(99.99, "USD")
const tax = new Money(8.99, "USD")
console.log(price + tax) // 108.98 (number: valueOf)
console.log(`${price}`) // "99.99 USD" (string: toString)
console.log(price > 50) // true (comparison: valueOf)
console.log(price * 2) // 199.98 (arithmetic: valueOf)Relational operators follow a slightly different coercion path than ==:
"apple" < "banana" // true (lexicographic)
"b" > "a" // true
"10" > "9" // false (lexicographic! "1" < "9")
"abc" < "abd" // true (compares char by char)"10" > 9 // true ("10"→10, 10>9)
"10" > "9" // false (both strings → lexicographic: "1"<"9")
null > 0 // false (null→0, 0>0 = false)
null < 0 // false (null→0, 0<0 = false)
null == 0 // false (special rule: null only == null/undefined)
null >= 0 // true ← SURPRISING! (null→0, 0>=0 = true)
undefined > 0 // false (undefined→NaN, NaN comparisons = false)
undefined < 0 // false
undefined == 0 // false
// String vs Number
"5" > 3 // true ("5"→5, 5>3)
"5" < "30" // false (lexicographic: "5" > "3")
5 > "30" // false (5 > 30 = false)null > 0 // false
null == 0 // false
null >= 0 // true ← Wait, what??
// Explanation:
// > and < convert null to 0 via ToNumber
// null > 0 → 0 > 0 → false
// null >= 0 → 0 >= 0 → true
// But == uses a special rule: null only == null or undefined
// So null == 0 → false (without type conversion)// || returns the FIRST truthy value, or the last value
"hello" || "world" // "hello"
"" || "world" // "world"
0 || false || "hi" // "hi"
0 || false || null // null (last value if all falsy)
false || 0 // 0
// && returns the FIRST falsy value, or the last value
"hello" && "world" // "world"
"hello" && null // null
null && "world" // null
1 && 2 && 3 // 3 (all truthy, returns last)
1 && 0 && 3 // 0 (returns first falsy)// Default values (pre-nullish coalescing)
function greet(name) {
name = name || "Guest" // fallback if name is falsy
return `Hello, ${name}!`
}
greet("Alice") // "Hello, Alice!"
greet("") // "Hello, Guest!" ← "" is falsy
greet(0) // "Hello, Guest!" ← 0 is falsy (bug if 0 is valid!)
// Guard clause with &&
const user = { name: "Alice", address: { city: "NYC" } }
const city = user && user.address && user.address.city // "NYC"
const noAddress = { name: "Bob" }
const city2 = noAddress && noAddress.address && noAddress.address.city // undefined
// Short-circuit evaluation (prevents execution)
const obj = null
obj && obj.doSomething() // null — doSomething() never called
// Conditional rendering pattern (React-like)
const isLoggedIn = true
const element = isLoggedIn && "<UserDashboard />" // "<UserDashboard />"Introduced in ES2020, ?? only falls back when the value is null or undefined — not other falsy values.
// ?? vs ||
0 || "default" // "default" (0 is falsy)
0 ?? "default" // 0 (0 is not null/undefined!)
"" || "default" // "default" ("" is falsy)
"" ?? "default" // "" ("" is not null/undefined!)
false || "default" // "default"
false ?? "default" // false
null || "default" // "default"
null ?? "default" // "default" (same here)
undefined || "default" // "default"
undefined ?? "default" // "default" (same here)const config = {
port: 0, // 0 is a valid port!
debug: false, // false is a valid setting!
timeout: null // explicitly no timeout
}
// Bug-prone with ||
const port = config.port || 3000 // 3000 — BUG! 0 is valid
const debug = config.debug || true // true — BUG! false is valid
// Correct with ??
const port2 = config.port ?? 3000 // 0 ✅
const debug2 = config.debug ?? false // false ✅
const timeout = config.timeout ?? 5000 // 5000 ✅
// Optional chaining + ??
const user = null
const city = user?.address?.city ?? "Unknown" // "Unknown"Template literals always coerce values to strings using the toString() method.
const num = 42
const arr = [1, 2, 3]
const obj = { x: 1 }
const sym = Symbol("s")
`Value: ${num}` // "Value: 42"
`Array: ${arr}` // "Array: 1,2,3"
`Object: ${obj}` // "Object: [object Object]"
`Null: ${null}` // "Null: null"
`Undef: ${undefined}` // "Undef: undefined"
`Bool: ${true}` // "Bool: true"
// Symbol throws in template literals!
// `Sym: ${sym}` // TypeError: Cannot convert a Symbol to a string
`Sym: ${sym.toString()}` // "Sym: Symbol(s)" ← must be explicit
// Custom toString gives control
class Point {
constructor(x, y) { this.x = x; this.y = y }
toString() { return `(${this.x}, ${this.y})` }
}
const p = new Point(3, 4)
`Point is: ${p}` // "Point is: (3, 4)"function highlight(strings, ...values) {
return strings.reduce((result, str, i) => {
const val = values[i - 1]
return result + `[${val}]` + str
})
}
const name = "Alice"
const age = 30
highlight`Name: ${name} and Age: ${age}` // "Name: [Alice] and Age: [30]"Arrays have some of the most surprising coercion behavior in JavaScript.
// Array → String (join with comma)
[].toString() // ""
[1].toString() // "1"
[1, 2, 3].toString() // "1,2,3"
[[1, 2], [3]].toString() // "1,2,3" (nested arrays flattened)
// Array → Number
Number([]) // 0 ([]→""→0)
Number([1]) // 1 ([1]→"1"→1)
Number([1, 2]) // NaN ([1,2]→"1,2"→NaN)
Number([""]) // 0 ([""]→""→0)
// The wild comparisons
[] == ![] // true ← one of JS's most famous WTFs!
// Explanation:
// ![] → !true → false ([] is truthy, so ![] is false)
// [] == false → (step 6 in abstract equality)
// [] == ToNumber(false) → [] == 0
// ToPrimitive([]) == 0 → "" == 0
// ToNumber("") == 0 → 0 == 0 → true!
[] + [] // "" (both → "")
[] + {} // "[object Object]"
{} + [] // 0 (block + unary +)
[1] + [2] // "12" ("1" + "2")
[1, 2] + [3, 4] // "1,23,4"
// Sorting without comparator — coerces to strings!
[10, 9, 2, 1, 100].sort() // [1, 10, 100, 2, 9] ← lexicographic!
[10, 9, 2, 1, 100].sort((a, b) => a - b) // [1, 2, 9, 10, 100] ✅Symbols are designed to NOT coerce silently — they throw errors.
const sym = Symbol("mySymbol")
// String coercion throws
String(sym) // "Symbol(mySymbol)" ← explicit OK
sym.toString() // "Symbol(mySymbol)"
`${sym}` // TypeError! ← implicit throws
sym + "" // TypeError! ← implicit throws
sym + "string" // TypeError!
// Number coercion throws
Number(sym) // TypeError!
+sym // TypeError!
sym + 1 // TypeError!
// Boolean coercion works (always true)
Boolean(sym) // true
!!sym // true
if (sym) {} // no errorconst big = 42n
// Arithmetic with BigInt
big + 1n // 43n ✅
big * 2n // 84n ✅
big + 1 // TypeError! Cannot mix BigInt and Number
// Explicit conversion
Number(42n) // 42 ← loses precision for huge values
BigInt(42) // 42n
BigInt("100") // 100n
// String coercion works
String(42n) // "42"
`${42n}` // "42"
42n + "" // "42"
// Comparison (works without coercion issue)
42n == 42 // true (loose equality with coercion)
42n === 42 // false (strict: different types)
42n > 10 // true (relational works across types)
42n < 100 // true
// Boolean coercion
Boolean(0n) // false
Boolean(42n) // true
!!0n // false// Always specify radix — old browsers treated "0x" as hex, "0" as octal
parseInt("010") // 8 in old JS (octal), 10 in modern JS
parseInt("010", 10) // 10 — always explicit ✅
parseInt("0x10") // 16 — hex (this one is safe)
parseInt("010", 8) // 8 — explicit octaltypeof NaN // "number" — NaN is technically a number type!
isNaN("hello") // true — coerces to NaN first!
isNaN(undefined) // true
isNaN(null) // false (null → 0)
// Better: use Number.isNaN (no coercion)
Number.isNaN("hello") // false ✅
Number.isNaN(NaN) // true ✅
Number.isNaN(undefined) // false ✅
// Or Object.is
Object.is(NaN, NaN) // true ✅const arr = [1, NaN, null, undefined]
arr.indexOf(NaN) // -1 ← NaN !== NaN
arr.includes(NaN) // true ← uses Object.is internally ✅
arr.indexOf(null) // 1 ✅
arr.includes(null) // true ✅// Empty array is TRUTHY
if ([]) console.log("truthy") // logs "truthy"!
// But when compared to false...
[] == false // true!
// This creates the paradox:
if ([]) {
// This runs ([] is truthy)
}
if ([] == false) {
// This also runs ([] == false is true)
}{} + 1 // 1 — {} is a block statement!
({}) + 1 // "[object Object]1"
({}) - 1 // NaN — but - doesn't have this ambiguitynull > 0 // false
null < 0 // false
null == 0 // false
null >= 0 // true ← !!!
null <= 0 // true ← !!!
// Rule: null only == null or undefined with ==
// But >= and <= convert null to 0 firstconst x = "1"
switch (x) {
case 1: // uses ===, so "1" !== 1
console.log("number one")
break
case "1": // "1" === "1" ✅
console.log("string one") // This runs!
break
}// ❌ Risky
if (x == null) {} // true for both null and undefined
// ✅ Intentional (the one valid use of ==)
if (x == null) {} // OK if you want to catch null OR undefined
// ✅ Explicit
if (x === null || x === undefined) {}
if (x == null) {} // shorthand for above, acceptable// ❌
isNaN("hello") // true — misleading!
// ✅
Number.isNaN("hello") // false — "hello" is not NaN, it's a string
Number.isNaN(NaN) // true// ❌ Loses valid falsy values
const port = config.port || 3000 // wrong when port = 0
// ✅
const port = config.port ?? 3000 // correct// ❌ Implicit and surprising
const total = items.length + ""
const count = +"5"
// ✅ Explicit and clear
const total = String(items.length)
const count = Number("5")
// or
const count = parseInt("5", 10)// ❌
parseInt("10") // works, but intention unclear
// ✅
parseInt("10", 10) // decimal
parseInt("ff", 16) // hex
parseInt("101", 2) // binary// ❌ Sorts lexicographically
[10, 2, 100, 1].sort()
// ✅ Sorts numerically
[10, 2, 100, 1].sort((a, b) => a - b) // ascending
[10, 2, 100, 1].sort((a, b) => b - a) // descending| Value | Number() result |
|---|---|
true |
1 |
false |
0 |
null |
0 |
undefined |
NaN |
"" |
0 |
" " |
0 |
"42" |
42 |
"3.14" |
3.14 |
"abc" |
NaN |
"0x1F" |
31 |
[] |
0 |
[1] |
1 |
[1,2] |
NaN |
{} |
NaN |
| Value | Boolean() result |
|---|---|
false |
false |
0 |
false |
-0 |
false |
0n |
false |
"" |
false |
null |
false |
undefined |
false |
NaN |
false |
| everything else | true |
| Expression | Result | Why |
|---|---|---|
1 + "2" |
"12" |
String concatenation |
1 - "2" |
-1 |
Numeric subtraction |
true + 1 |
2 |
true → 1 |
false + 1 |
1 |
false → 0 |
null + 1 |
1 |
null → 0 |
undefined + 1 |
NaN |
undefined → NaN |
[] + [] |
"" |
Both → "" |
[] + {} |
"[object Object]" |
[]→"", {}→"[object Object]" |
!!"" |
false |
"" is falsy |
!![] |
true |
[] is truthy |
[] == false |
true |
[]→""→0, false→0 |
null == undefined |
true |
Special rule |
null == 0 |
false |
null only equals null/undefined |
NaN == NaN |
false |
NaN is never equal to itself |
Golden Rule: When in doubt, be explicit. Use
Number(),String(),Boolean(),===, and??to make your intent clear to other developers — and to your future self.