docs/concepts/type-coercion.mdx
Why does "5" + 3 give you "53" but "5" - 3 gives you 2? Why does [] == ![] return true? How does JavaScript decide what type a value should be?
// JavaScript's "helpful" type conversion in action
console.log("5" + 3); // "53" (string concatenation!)
console.log("5" - 3); // 2 (numeric subtraction)
console.log([] == ![]); // true (wait, what?!)
This surprising behavior is type coercion. JavaScript automatically converts values from one type to another. Understanding these rules helps you avoid bugs and write more predictable code.
<Info> **What you'll learn in this guide:** - The difference between implicit and explicit coercion - How JavaScript converts to strings, numbers, and booleans - The 8 falsy values every developer must memorize - How objects convert to primitives - The famous JavaScript "WAT" moments explained - Best practices for avoiding coercion bugs </Info> <Warning> **Prerequisites:** This guide assumes you understand [Primitive Types](/concepts/primitive-types). If terms like string, number, boolean, null, and undefined are new to you, read that guide first! </Warning>Type coercion is the automatic or implicit conversion of values from one data type to another in JavaScript. According to the ECMAScript specification, JavaScript performs these conversions through a set of "abstract operations" — internal algorithms like ToString, ToNumber, and ToBoolean. When you use operators or functions that expect a certain type, JavaScript will convert (coerce) values to make the operation work, sometimes helpfully, sometimes surprisingly. Understanding these conversion rules helps you write predictable, bug-free code.
Imagine JavaScript as an overly helpful translator. When you give it values of different types, it tries to "help" by converting them, sometimes correctly, sometimes... creatively.
┌─────────────────────────────────────────────────────────────────────────┐
│ THE OVERLY HELPFUL TRANSLATOR │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ YOU: "Hey JavaScript, add 5 and '3' together" │
│ │
│ JAVASCRIPT (thinking): "Hmm, one's a number, one's a string... │
│ I'll just convert the number to a string! │
│ '5' + '3' = '53'. You're welcome!" │
│ │
│ YOU: "That's... not what I meant." │
│ │
│ JAVASCRIPT: "¯\_(ツ)_/¯" │
│ │
└─────────────────────────────────────────────────────────────────────────┘
This "helpful" behavior is called type coercion. JavaScript automatically converts values from one type to another. Sometimes it's useful, sometimes it creates bugs that will haunt your dreams.
┌─────────────────────────────────────────────────────────────────────────┐
│ TYPE COERCION: THE SHAPESHIFTER │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ ┌─────────┐ │
│ │ "5" │ ──── + 3 ────────► │ "53" │ String won! │
│ │ string │ │ string │ │
│ └─────────┘ └─────────┘ │
│ │
│ ┌─────────┐ ┌─────────┐ │
│ │ "5" │ ──── - 3 ────────► │ 2 │ Number won! │
│ │ string │ │ number │ │
│ └─────────┘ └─────────┘ │
│ │
│ Same values, different operators, different results! │
│ │
└─────────────────────────────────────────────────────────────────────────┘
There are two ways coercion happens:
<Tabs> <Tab title="Explicit Coercion"> **You** control the conversion using built-in functions. This is predictable and intentional.```javascript
// YOU decide when and how to convert
Number("42") // 42
String(42) // "42"
Boolean(1) // true
parseInt("42px") // 42
parseFloat("3.14") // 3.14
```
These functions — [`Number()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number), [`String()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String), [`Boolean()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean), [`parseInt()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt), and [`parseFloat()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseFloat) — give you full control.
This is the **safe** way. You know exactly what's happening.
```javascript
// JavaScript "helps" without asking
"5" + 3 // "53" (number became string)
"5" - 3 // 2 (string became number)
if ("hello") {} // string became boolean (true)
5 == "5" // true (types were coerced)
```
This is where bugs hide. Learn the rules or suffer the consequences!
JavaScript is a dynamically typed language. Variables don't have fixed types. This flexibility means JavaScript needs to figure out what to do when types don't match.
// In JavaScript, variables can hold any type
let x = 42; // x is a number
x = "hello"; // now x is a string
x = true; // now x is a boolean
// So what happens here?
let result = x + 10; // JavaScript must decide how to handle this
Other languages would throw an error. JavaScript tries to make it work. Whether that's a feature or a bug... depends on who you ask!
Here's the most important rule: JavaScript can only convert to THREE primitive types:
| Target Type | Explicit Method | Common Implicit Triggers |
|---|---|---|
| String | String(value) | + with a string, template literals |
| Number | Number(value) | Math operators (- * / %), comparisons |
| Boolean | Boolean(value) | if, while, !, &&, ||, ? : |
That's it. No matter how complex the coercion seems, the end result is always a string, number, or boolean.
┌─────────────────────────────────────────────────────────────────────────┐
│ THE THREE CONVERSION DESTINATIONS │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────┐ │
│ │ ANY VALUE │ │
│ │ (string, number, │ │
│ │ object, array...) │ │
│ └──────────┬───────────┘ │
│ │ │
│ ┌────────────────┼────────────────┐ │
│ ▼ ▼ ▼ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ String │ │ Number │ │ Boolean │ │
│ │ "42" │ │ 42 │ │ true │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
│ These are the ONLY three possible destinations! │
│ │
└─────────────────────────────────────────────────────────────────────────┘
String conversion is the most straightforward. Almost anything can become a string.
// Explicit conversion
String(123) // "123"
String(true) // "true"
(123).toString() // "123"
// Implicit conversion
123 + "" // "123" (concatenation with empty string)
`Value: ${123}` // "Value: 123" (template literal)
"Hello " + 123 // "Hello 123" (+ with a string)
The toString() method and template literals are also common ways to convert values to strings.
| Value | Result | Notes |
|---|---|---|
123 | "123" | Numbers become digit strings |
-12.34 | "-12.34" | Decimals and negatives work too |
true | "true" | Booleans become their word |
false | "false" | |
null | "null" | |
undefined | "undefined" | |
[1, 2, 3] | "1,2,3" | Arrays join with commas |
[] | "" | Empty array becomes empty string |
{} | "[object Object]" | Objects become this (usually useless) |
Symbol("id") | Throws TypeError! | Symbols can't implicitly convert |
The + operator is special: it does both addition and concatenation:
// With two numbers: addition
5 + 3 // 8
// With any string involved: concatenation
"5" + 3 // "53" (3 becomes "3")
5 + "3" // "53" (5 becomes "5")
"Hello" + " World" // "Hello World"
// Order matters with multiple operands!
1 + 2 + "3" // "33" (1+2=3, then 3+"3"="33")
"1" + 2 + 3 // "123" (all become strings left-to-right)
// Dangerous: user input is always a string!
const userInput = "5";
const result = userInput + 10; // "510", not 15!
// Safe: convert first
const result = Number(userInput) + 10; // 15
Number conversion has more triggers than string conversion, and more edge cases to memorize.
// Explicit conversion
Number("42") // 42
parseInt("42px") // 42 (stops at non-digit)
parseFloat("3.14") // 3.14
+"42" // 42 (unary plus trick)
// Implicit conversion
"6" - 2 // 4 (subtraction)
"6" * 2 // 12 (multiplication)
"6" / 2 // 3 (division)
"6" % 4 // 2 (modulo)
"10" > 5 // true (comparison)
+"42" // 42 (unary plus)
| Value | Result | Notes |
|---|---|---|
"123" | 123 | Numeric strings work |
" 123 " | 123 | Whitespace is trimmed |
"123abc" | NaN | Any non-numeric char → NaN |
"" | 0 | Empty string becomes 0 |
" " | 0 | Whitespace-only becomes 0 |
true | 1 | |
false | 0 | |
null | 0 | null → 0 |
undefined | NaN | undefined → NaN (different!) |
[] | 0 | Empty array → "" → 0 |
[1] | 1 | Single element array |
[1, 2] | NaN | Multiple elements → NaN |
{} | NaN | Objects → NaN |
Number(null) // 0
Number(undefined) // NaN
null + 5 // 5
undefined + 5 // NaN
Unlike +, the other math operators (-, *, /, %) only do math. They always convert to numbers:
"6" - "2" // 4 (both become numbers)
"6" * "2" // 12
"6" / "2" // 3
"10" % "3" // 1
// This is why - and + behave differently!
"5" + 3 // "53" (concatenation)
"5" - 3 // 2 (math)
The unary + (plus sign before a value) is a quick way to convert to a number:
+"42" // 42
+true // 1
+false // 0
+null // 0
+undefined // NaN
+"hello" // NaN
+"" // 0
Boolean conversion is actually the simplest. Every value is either truthy or falsy.
// Explicit conversion
Boolean(1) // true
Boolean(0) // false
!!value // double negation trick
// Implicit conversion
if (value) { } // condition check
while (value) { } // loop condition
value ? "yes" : "no" // ternary operator
value && doSomething() // logical AND
value || defaultValue // logical OR
!value // logical NOT
As documented in MDN's reference on falsy values, there are 8 common values that convert to false. Everything else is true.
// THE FALSY EIGHT
Boolean(false) // false (obviously)
Boolean(0) // false
Boolean(-0) // false (yes, -0 exists)
Boolean(0n) // false ([BigInt](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt) zero)
Boolean("") // false (empty string)
Boolean(null) // false
Boolean(undefined) // false
Boolean(NaN) // false
This includes some surprises:
// These are all TRUE!
Boolean(true) // true (obviously)
Boolean(1) // true
Boolean(-1) // true (negative numbers!)
Boolean("hello") // true
Boolean("0") // true (non-empty string!)
Boolean("false") // true (non-empty string!)
Boolean([]) // true (empty array!)
Boolean({}) // true (empty object!)
Boolean(function(){}) // true
Boolean(new Date()) // true
Boolean(Infinity) // true
Boolean(-Infinity) // true
// These catch people ALL the time:
Boolean("0") // true (it's a non-empty string!)
Boolean("false") // true (it's a non-empty string!)
Boolean([]) // true (arrays are objects, objects are truthy)
Boolean({}) // true (even empty objects)
// If checking for empty array, do this:
if (arr.length) { } // checks if array has items
if (arr.length === 0) { } // checks if array is empty
A common misconception: && and || don't necessarily return true or false. They return one of the original values:
// || returns the FIRST truthy value (or the last value)
"hello" || "world" // "hello"
"" || "world" // "world"
"" || 0 || null || "yes" // "yes"
// && returns the FIRST falsy value (or the last value)
"hello" && "world" // "world"
"" && "world" // ""
1 && 2 && 3 // 3
// This is useful for defaults!
const name = userInput || "Anonymous";
const display = user && user.name;
When JavaScript needs to convert an object to a primitive (including arrays), it follows a specific algorithm.
// Arrays - toString() returns joined elements
[1, 2, 3].toString() // "1,2,3"
[1, 2, 3] + "" // "1,2,3"
[1, 2, 3] - 0 // NaN (can't convert "1,2,3" to number)
[].toString() // ""
[] + "" // ""
[] - 0 // 0 (empty string → 0)
[1].toString() // "1"
[1] - 0 // 1
// Plain objects - toString() returns "[object Object]"
({}).toString() // "[object Object]"
({}) + "" // "[object Object]"
// Dates - special case, prefers string for + operator
const date = new Date(0);
date.toString() // "Thu Jan 01 1970 ..."
date.valueOf() // 0 (timestamp in ms)
date + "" // "Thu Jan 01 1970 ..." (uses toString)
date - 0 // 0 (uses valueOf)
You can control how your objects convert:
const price = {
amount: 99.99,
currency: "USD",
valueOf() {
return this.amount;
},
toString() {
return `${this.currency} ${this.amount}`;
}
};
// Number conversion uses valueOf()
price - 0 // 99.99
price * 2 // 199.98
+price // 99.99
// String conversion uses toString()
String(price) // "USD 99.99"
`Price: ${price}` // "Price: USD 99.99"
// + is ambiguous, uses valueOf() if it returns primitive
price + "" // "99.99" (valueOf returned number, then → string)
ES6 introduced a cleaner way to control conversion — Symbol.toPrimitive:
const obj = {
[Symbol.toPrimitive](hint) {
console.log(`Converting with hint: ${hint}`);
if (hint === "number") {
return 42;
}
if (hint === "string") {
return "forty-two";
}
// hint === "default"
return "default value";
}
};
+obj // 42 (hint: "number")
`${obj}` // "forty-two" (hint: "string")
obj + "" // "default value" (hint: "default")
The loose equality operator == is where type coercion gets wild. For a deeper dive into all equality operators, see our Equality Operators guide. Here's how == actually works:
true == "true"
// step 1: 1 == "true" (true → 1)
// step 2: 1 == NaN ("true" → NaN)
// result: false (surprise!)
```
// Example 1: "5" == 5
"5" == 5
// String vs Number → convert string to number
// 5 == 5
// Result: true
// Example 2: true == "1"
true == "1"
// Boolean involved → convert boolean to number first
// 1 == "1"
// Number vs String → convert string to number
// 1 == 1
// Result: true
// Example 3: [] == false
[] == false
// Boolean involved → convert boolean to number first
// [] == 0
// Object vs Number → convert object to primitive
// "" == 0 (empty array → empty string)
// String vs Number → convert string to number
// 0 == 0
// Result: true
// Example 4: [] == ![]
[] == ![]
// First, evaluate ![] → false (arrays are truthy)
// [] == false
// Boolean involved → false becomes 0
// [] == 0
// Object vs Number → [] becomes ""
// "" == 0
// String vs Number → "" becomes 0
// 0 == 0
// Result: true (yes, really!)
5 === "5" // false (different types)
5 == "5" // true (coerced)
null === undefined // false
null == undefined // true
Quick reference for which operators trigger which coercion:
| Operator | Coercion Type | Example | Result |
|---|---|---|---|
+ (with string) | String | "5" + 3 | "53" |
+ (unary) | Number | +"5" | 5 |
- * / % | Number | "5" - 3 | 2 |
++ -- | Number | let x = "5"; x++ | 6 |
> < >= <= | Number | "10" > 5 | true |
== != | Complex | "5" == 5 | true |
=== !== | None | "5" === 5 | false |
&& || | Boolean (internal) | "hi" || "bye" | "hi" |
! | Boolean | !"hello" | false |
if while ? : | Boolean | if ("hello") | true |
& | ^ ~ | Number (32-bit int) | "5" | 0 | 5 |
Let's explore the famous "weird parts" that make JavaScript... special.
<AccordionGroup> <Accordion title="1. The + Operator's Split Personality"> ```javascript "5" + 3 // "53" (string concatenation) "5" - 3 // 2 (math!)// Why? + does both addition AND concatenation
// If either operand is a string, it concatenates
// - only does subtraction, so it converts to numbers
```
[] + {} // "[object Object]"
// [] → "", {} → "[object Object]"
{} + [] // 0 (in browser console!)
// {} is parsed as empty block, then +[] = 0
// Wrap in parens to fix: ({}) + [] = "[object Object]"
```
// Booleans convert to 1 (true) or 0 (false)
```
// Step by step:
// 1. ![] → false (arrays are truthy, negated = false)
// 2. [] == false
// 3. [] == 0 (boolean → number)
// 4. "" == 0 (array → string)
// 5. 0 == 0 (string → number)
// 6. true!
// Meanwhile...
[] === ![] // false (different types, no coercion)
```
// Step by step:
// 1. +"bar" is evaluated first (unary +)
// 2. +"bar" → NaN (can't convert "bar" to number)
// 3. "foo" + NaN → "fooNaN"
```
// NaN is the only value in JavaScript not equal to itself!
// This is by design (IEEE 754 spec)
// How to check for NaN:
Number.isNaN(NaN) // true (correct way)
isNaN(NaN) // true
isNaN("hello") // true (wrong! it converts first)
Number.isNaN("hello") // false (correct)
```
Use [`Number.isNaN()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN) instead of the global [`isNaN()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/isNaN) for reliable NaN checking.
// Arrays convert to strings:
// [1, 2] → "1,2"
// [3, 4] → "3,4"
// "1,2" + "3,4" → "1,23,4"
// To actually combine arrays:
[...[1, 2], ...[3, 4]] // [1, 2, 3, 4]
[1, 2].concat([3, 4]) // [1, 2, 3, 4]
```
=== instead of == — No surprises, no coercionNumber(), String(), Boolean() when convertingNumber.isNaN() — Not isNaN() or === NaN+ — Remember it concatenates if any operand is a string
</Tip>
Stack Overflow's 2023 Developer Survey reports that type-related bugs remain among the most common debugging challenges for JavaScript developers. Despite the gotchas, some implicit coercion patterns are actually helpful:
// 1. Checking for null OR undefined in one shot
if (value == null) {
// This catches BOTH null and undefined
// Much cleaner than: if (value === null || value === undefined)
}
// 2. Boolean context is natural and readable
if (user) {
// Truthy check - totally fine
}
if (items.length) {
// Checking if array has items - totally fine
}
// 3. Quick string conversion
const str = value + "";
// or
const str = String(value);
// or
const str = `${value}`;
// 4. Quick number conversion
const num = +value;
// or
const num = Number(value);
// BAD: Relying on == for type-unsafe comparisons
if (x == true) { } // Don't do this!
if (x) { } // Do this instead
// BAD: Using == with 0 or ""
if (x == 0) { } // Matches "", but not null (null == 0 is false!)
if (x === 0) { } // Clear intent
// BAD: Truthy check when you need specific type
function process(count) {
if (!count) return; // Fails for count = 0!
// ...
}
function process(count) {
if (typeof count !== "number") return; // Better
// ...
}
Three conversions only — JavaScript converts to String, Number, or Boolean — nothing else
Implicit vs Explicit — Know when JS converts automatically vs when you control it
The 8 common falsy values — false, 0, -0, 0n, "", null, undefined, NaN — everything else is truthy (plus the rare document.all)
+ is special — It prefers string concatenation if ANY operand is a string
- * / % are consistent — They ALWAYS convert to numbers
== coerces, === doesn't — Use === by default to avoid surprises
null == undefined — This is true, but neither equals anything else with ==
Objects convert via valueOf() and toString() — Learn these methods to control conversion
When in doubt, be explicit — Use Number(), String(), Boolean()
NaN is unique — It's the only value not equal to itself; use Number.isNaN() to check
The `+` operator, when one operand is a string, performs string concatenation. The number `3` is converted to `"3"`, resulting in `"5" + "3" = "53"`.
Everything else is truthy, including `[]`, `{}`, `"0"`, and `"false"`.
**Bonus:** There's also a 9th falsy value — `document.all` — a legacy browser API you'll rarely encounter.
1. `![]` evaluates first: arrays are truthy, so `![]` = `false`
2. Now we have `[] == false`
3. Boolean converts to number: `[] == 0`
4. Array converts to primitive: `"" == 0`
5. String converts to number: `0 == 0`
6. Result: `true`
- `===` (strict equality) **never** coerces. If types differ, it returns `false` immediately.
- `==` (loose equality) **coerces** values to the same type before comparing, following a complex algorithm.
```javascript
5 === "5" // false (different types)
5 == "5" // true (string coerced to number)
```
Best practice: Use `===` unless you specifically need coercion.
```javascript
Number(null) // 0
Number(undefined) // NaN
```
This inconsistency is a common source of bugs. `null` converts to `0` (like "nothing" = zero), while `undefined` converts to `NaN` (like "no value" = not a number).
Step by step:
1. `true + false` = `1 + 0` = `1` (booleans → numbers)
2. `1 + "hello"` = `"1hello"` (number → string for concatenation)