Understanding type coercion: the hidden rule that makes "5" + 3 equal "53" and explains every confusing bug you'll ever hit.

Picture this: you've built a small program that asks a user to enter two numbers and adds them together. You test it. The user types 10 and 5. You expect 15. You get "105".
You stare at the screen. You check your code. The addition sign is right there. You didn't change anything. JavaScript just... did that. On its own.
This is the moment every JavaScript beginner hits, usually around day 3 or 4, where the language feels like it's actively working against you. But here's the thing: JavaScript didn't make a mistake. It followed a very specific set of rules. You just didn't know about those rules yet.
Those rules are called type coercion. And once you understand them, that "105" result stops being a mystery and starts being completely obvious. That's what this article is about.
Why JavaScript Plays Fast and Loose with Your Data
Your Calculator Doesn't Do This, So Why Does JavaScript?
Open any basic calculator app on your phone. Try typing 5 + and then a word. It won't let you. Calculators are rigid: numbers go in, numbers come out. If you break the rules, the calculator refuses to cooperate.
JavaScript is the opposite of a calculator.
In JavaScript, a variable doesn't have a fixed type. It doesn't get stamped with a permanent "number" or "text" label when you create it. Watch what you can do:
let x = 42; // x is a number
x = "hello"; // now x is text
x = true; // now x is a boolean
x = null; // now x is nullNo errors. No complaints. JavaScript just rolls with it. This is called being dynamically typed: the type of a value is determined by what's inside it at any given moment, not by a rigid declaration you made at the start.
Most of the time, this is genuinely useful. You write code faster. You don't have to think about types for every single variable. Things just work.
But here's where it gets complicated. What happens when two different types meet in an operation?
let result = x + 10;If x is true at this point, what should result be? true + 10 doesn't mean anything in math. But JavaScript won't throw an error and give up. Instead, it makes a decision. It converts one of the values to a type that makes the operation possible and then carries on.
That automatic conversion is type coercion. JavaScript looks at the situation, picks a conversion rule from its internal playbook, applies it, and gives you a result. The entire problem is that it does this silently. No warning. No message in the console. The conversion just happens, and you get a result that might be completely wrong for what you intended.
That's why this concept matters. Not because coercion is evil, but because it's invisible, and invisible rules are the hardest ones to debug.
Two Kinds of Coercion: One You Control, One You Don't
Before we get into the specific rules, there's a clean way to split coercion into two buckets.
Implicit coercion is when JavaScript converts a type on its own, without you asking. You write an operation, JavaScript sees a type mismatch, and it quietly converts one of the values behind the scenes.
// You didn't ask for any conversion here.
// JavaScript converted "5" to a number on its own.
"5" - 3 // 2
//
// You didn't ask for conversion here either.
// JavaScript converted 10 to a string on its own.
10 + " items" // "10 items"
//
// if() checks need a boolean.
// JavaScript converted "hello" to true on its own.
if ("hello") {
console.log("this runs");
}Explicit coercion is when you do the conversion yourself, on purpose, using JavaScript's built-in tools. You're in the driver's seat. You decided to convert. You know it happened.
// YOU decided to convert these. Crystal clear.
Number("42") // 42
String(42) // "42"
Boolean(0) // falseThe difference isn't about which one is "correct." Implicit coercion isn't always bad: there are cases where it's totally fine. The real problem is not knowing it happened. When you write "5" - 3 and get 2, that's implicit coercion doing its job correctly. But if you expected a string and got a number, or expected a number and got NaN, the bug is invisible because nothing told you a conversion occurred.
The rest of this article is about learning the rules so that you always know which case you're in.
The One Operator That Wears Two Hats
When a Plus Sign Doesn't Add
Here's the most common coercion surprise in JavaScript, and it comes from a single operator you've probably already used: the + sign.
Every other math operator (subtraction, multiplication, division, the remainder %) has one job. They do math. If something isn't a number, they convert it to a number and then do math. Simple.
The + operator has two jobs. It adds numbers, sure. But it also joins strings together (this is called concatenation). And when it sees values on both sides, it has to decide which job to do.
Here's the rule: if either value is a string, + picks the string job. The number (if there is one) gets converted to a string, and they get joined.
// Two numbers: addition happens
5 + 3 // 8
//
// One string involved: string wins, concatenation happens
"5" + 3 // "53" <--- 3 becomes "3", then joined
5 + "3" // "53" <--- 5 becomes "5", then joined
//
// Completely normal string joining
"Hello " + "World" // "Hello World"So far, it's a bit weird but manageable. The real trap comes when you chain multiple values together. JavaScript reads + operations from left to right, one pair at a time.
// Left-to-right reading:
// Step 1: 1 + 2 → 3 (both numbers, math happens)
// Step 2: 3 + "3" → "33" (string involved, join happens)
1 + 2 + "3" // "33"
//
// Left-to-right reading:
// Step 1: "1" + 2 → "12" (string involved, join happens)
// Step 2: "12" + 3 → "123" (string involved, join happens)
"1" + 2 + 3 // "123"Same values. Different order. Completely different results. That's not a bug: that's exactly how the rules work. Once a string appears in the chain, everything from that point forward gets treated as string concatenation.
Now here's the real-world version of this problem. When a user types something into a form or an input box on a webpage, whatever they type comes back to your code as a string. Always. Even if they typed the number 10, your code receives it as the string "10".
// User typed 10 into a form. You received a string.
const userInput = "10";
//
// You expected addition. You got concatenation.
const stringTotal = userInput + 5;
console.log(stringTotal); // "105" <--- not 15!
//
// The fix: convert first, then add
const correctTotal = Number(userInput) + 5;
console.log(correctTotal); // 15This exact bug has burned almost every JavaScript developer at least once. Now you know why it happens and exactly how to fix it.
The Other Operators Don't Have This Problem
Here's the good news: every other math operator is completely consistent.
Subtraction (-), multiplication (*), division (/), and remainder (%) have no string mode. They are math-only operators. When you use any of them, JavaScript converts everything to a number first, then does the math.
"6" - 2 // 4 ("6" becomes 6, then 6 - 2)
"6" * 2 // 12 ("6" becomes 6, then 6 * 2)
"6" / 2 // 3 ("6" becomes 6, then 6 / 2)
"10" % 3 // 1 ("10" becomes 10, then 10 % 3)This is why the classic example "5" + 3 and "5" - 3 produce such different results:
"5" + 3 // "53" (+ sees a string, picks string mode)
"5" - 3 // 2 (- only does math, converts "5" to 5)Exactly the same values, exactly the same types, but the operator changes everything.
One more thing to know: if the string can't be converted to a sensible number, you get a special value called NaN, which stands for "Not a Number."
"hello" - 1 // NaN ("hello" can't become a number)
"cat" * 2 // NaN
"five" / 2 // NaNNaN is JavaScript's way of saying "this math broke." And once NaN appears in a calculation, it contaminates everything it touches:
NaN + 10 // NaN
NaN * 100 // NaN
5 - NaN // NaNYou'll see NaN again in the next section because it's also one of the seven falsy values, and it has a truly weird quirk you need to know about.
Every Value Is Either "Something" or "Nothing"
JavaScript Has a Simple Rule for True and False
Certain situations in JavaScript need a simple yes-or-no answer. When you write an if statement, JavaScript needs to know: should this block of code run or not? It asks that question by looking at whatever value you gave it and converting it to true or false.
This happens automatically: no action required from you. The conversion is called boolean coercion.
It shows up in several places:
// if() statements
if ("hello") {
console.log("runs: 'hello' is truthy");
}
//
// while() loops
while (countdown) {
countdown--;
}
//
// Ternary operator
const label = count ? "items" : "nothing";
//
// Logical operators
const name = userInput || "Anonymous";
const greeting = user && user.name;Every value in JavaScript falls into one of two buckets: truthy or falsy. Truthy values convert to true. Falsy values convert to false. There's no middle ground. No "kind of true" or "somewhat false."
The mental model that helps most beginners: truthy means "something is here." Falsy means "nothing is here." An empty container, a missing value, a broken number... those all signal "nothing." Everything else signals "something."
The list of falsy values is small enough to memorize. And that's exactly what we're going to do next.
The Seven Values JavaScript Calls "Nothing"
There are exactly seven distinct values that convert to false in JavaScript. Memorize these. Seriously: write them on a sticky note if you have to. Every other value in the language converts to true.
// THE SEVEN FALSY VALUES
Boolean(false) // false
Boolean(0) // false
Boolean(0n) // false
Boolean("") // false
Boolean(null) // false
Boolean(undefined) // false
Boolean(NaN) // falseLet's go through each one and understand why it's falsy, because memorizing without understanding doesn't stick.
false: The boolean literal itself. Obviously false.
0: The number zero. In almost every context, zero means "nothing," "none," "empty." Zero items. Zero dollars. Zero progress. JavaScript follows that same intuition. (Note: JavaScript also has a -0, negative zero, due to how computers store decimals under the hood. Both 0 and -0 evaluate as falsy, but they represent the same concept of zero).
0n: This is BigInt zero. BigInt is a special type in JavaScript for working with numbers so large they can't be stored as regular numbers. You probably won't use it in your first few weeks of learning, but just know that BigInt has its own version of zero, and it's falsy just like regular zero. We'll properly cover BigInt when we get to advanced number types.
"": The empty string. A string with zero characters in it. No content, no length, nothing to read. "Nothing" made of text.
Boolean("") // false: zero characters
Boolean(" ") // true : one space character! not empty!
Boolean("0") // true : one character! not empty!That last two lines are important. A string containing a single space is NOT empty: it has a character in it. A string containing the text "0" is NOT empty either: it has a character in it. Those are truthy, not falsy.
null: Intentional emptiness. When you set something to null, you're making a deliberate statement: "this variable exists, but right now it's empty." A programmer put null there on purpose.
let currentUser = null; // "No user is logged in right now."undefined: Accidental emptiness. undefined means a variable was created but was never given a value. It's JavaScript's way of saying "I know this name exists, but I have no idea what's inside it."
let username;
console.log(username); // undefined: nobody gave it a valueThe difference between null and undefined comes up constantly in interviews and in debugging. The short version: null is a deliberate absence. undefined is an unintentional absence.
NaN: "Not a Number." This appears when a math operation produces a result that isn't a valid number: like trying to do math with a word. And here's the truly strange quirk: NaN is the only value in JavaScript that is not equal to itself.
NaN === NaN // false: yes, really
NaN == NaN // false: still noEvery other value in the entire language equals itself. 5 === 5. "hello" === "hello". But NaN is broken by definition: it represents a failed number operation, and you can't reliably compare failed operations to each other. To actually check if something is NaN, you use a specific method:
Number.isNaN(NaN) // true : correct way
Number.isNaN("hello") // false: correct (it's a string, not NaN)
//
// Don't use the older isNaN() function: it converts first
isNaN("hello") // true : wrong! it converted "hello" to NaN firstA small footnote on an 8th falsy value: There's actually one more value that behaves as falsy, and it's called
document.all. It's an ancient browser API from the 1990s that exists purely so old websites don't break. You will almost certainly never encounter it in real code. But if someone ever asks you "what is the only object in JavaScript that is falsy?", now you know the trivia answer.
The Contradiction That Trips Everyone Up: Why [] Is Truthy
This is the moment where most beginners feel like JavaScript is genuinely broken.
An empty array [] has nothing in it. No items. Zero length. So it seems obvious that if ([]) should be the same as if (false), right? The array is empty. Nothing is inside. JavaScript should say "nothing here" and skip the code block.
But it doesn't. The code runs.
if ([]) {
console.log("this runs"); // this DOES run
}
//
Boolean([]) // true: confirmed truthyAnd then: making it even more confusing: this comparison evaluates to true:
if ([] == false) {
console.log("this also runs"); // this ALSO runs
}So [] is truthy... but [] == false is also true? That sounds impossible. How can something be both truthy and simultaneously equal to false?
The answer is that these two lines are doing completely different things.
if ([]) asks: "Does this value exist?" When JavaScript evaluates a value inside an if statement, it converts that value directly to boolean. Arrays are objects. Objects are not on the falsy list. Therefore, an array (even an empty one) is truthy. JavaScript isn't checking what's inside the array. It's checking whether the array itself exists. And it does. It's sitting right there in memory. So: truthy.
[] == false asks: "Are these two values loosely equal after coercion?" This is a completely different operation. The == operator doesn't convert to boolean. It runs a multi-step coercion algorithm that tries to convert both sides to the same type. Here's what actually happens:
// [] == false: what JavaScript actually does, step by step:
//
// Step 1: false is a boolean. Convert it to a number first.
// false → 0
// Now: [] == 0
//
// Step 2: [] is an object. Convert it to a primitive.
// [].toString() → "" (empty array becomes empty string)
// Now: "" == 0
//
// Step 3: "" is a string. Convert it to a number.
// Number("") → 0
// Now: 0 == 0
//
// Step 4: 0 === 0 → true
[] == false // trueSo these two things (a direct boolean check and a loose equality comparison) follow completely different rulebooks. if ([]) asks "does it exist?" [] == false goes through a chain of numeric conversions. The fact that they seem contradictory is exactly why you should almost never use == with non-primitive values. We'll cover that more at the end of this article.
Now, here's the thing beginners actually need to know: how do you check if an array is empty for real?
const cart = [];
//
// WRONG: this checks if the array EXISTS, not if it's empty
if (cart) {
console.log("cart is empty"); // runs even though cart is empty!
}
//
// RIGHT: check the length property
if (cart.length === 0) {
console.log("your cart is empty"); // only runs when actually empty
}
//
// ALSO RIGHT
if (!cart.length) {
console.log("your cart is empty");
}The same logic applies to objects. An empty object {} is also truthy, for the same reason: it exists in memory as an object, and objects aren't on the falsy list.
Boolean({}) // true: empty objects are truthy
Boolean([]) // true: empty arrays are truthy
Boolean("") // false: empty strings ARE falsyThis is not inconsistent. Strings and arrays are fundamentally different things to JavaScript. An empty string has zero content and is the "nothing" version of a string. An empty array is still a fully-formed array object that happens to have no items in it right now. Its emptiness is a property you can check: it's not what the object is.
The Hidden Mechanics: How Objects Turn Into Primitives
We just saw that [] == false is true because JavaScript secretly converts the empty array into an empty string, and then into a zero. But how exactly does an object (like an array) become a simple primitive value?
When JavaScript is forced to use an object in a math equation or a string concatenation, it runs an internal process. It asks the object to describe itself using two specific methods: valueOf() and toString().
valueOf(): JavaScript first asks, "Do you have a raw numeric value?" For most plain objects and arrays, the answer is no, so this step fails.toString(): IfvalueOf()fails, JavaScript asks, "Can you turn yourself into text?" For an array,[1, 2, 3].toString()becomes"1,2,3"and an empty array[].toString()becomes"". For a plain object,{}.toString()becomes the infamous"[object Object]".
This is why trying to add an object to a string produces results that look like absolute nonsense:
// The array converts to text: "1,2,3"
console.log([1, 2, 3] + ""); // "1,2,3"
//
// The object converts to text: "[object Object]"
console.log({} + " is weird"); // "[object Object] is weird"You do not need to memorize the entire algorithm yet. You just need to know that objects don't magically become numbers. They almost always convert themselves into text first, and that text is what gets used in your operation.
Taking Back Control: You Decide the Type
The Tools JavaScript Gives You to Be Explicit
The entire idea behind explicit coercion is simple: instead of letting JavaScript guess what type you want, you tell it. You pick the conversion function, you call it yourself, and you know exactly what you're getting back.
JavaScript has three main conversion functions for this. They're named exactly what they do.
Converting to a Number
Number() takes anything and tries to turn it into a number. If it can, it does. If it can't, you get NaN.
// Clean numeric strings work perfectly
Number("42") // 42
Number("3.14") // 3.14
Number("-7") // -7
//
// Whitespace gets trimmed automatically
Number(" 42 ") // 42
//
// Booleans have a simple mapping
Number(true) // 1
Number(false) // 0
//
// null and undefined are different: this trips people up
Number(null) // 0
Number(undefined) // NaN
//
// Empty string becomes zero
Number("") // 0
//
// Anything that can't be parsed
Number("hello") // NaN
Number("42abc") // NaN
Number([]) // 0 (empty array → "" → 0)That null vs undefined difference is a popular interview question. null was deliberately set to nothing, and JavaScript treats "nothing" as zero in a numeric context. undefined was never set at all, and "I don't even know" produces NaN: a number that isn't a number.
Converting to a String
String() takes anything and turns it into text. This one almost never surprises you.
String(42) // "42"
String(-3.14) // "-3.14"
String(true) // "true"
String(false) // "false"
String(null) // "null"
String(undefined) // "undefined"
String(NaN) // "NaN"
String([1, 2, 3]) // "1,2,3" (array elements joined by commas)
String([]) // "" (empty array → empty string)Converting to a Boolean
Boolean() takes anything and converts it to true or false. By now you know the rule: seven specific values produce false. Everything else produces true.
// The falsy ones
Boolean(false) // false
Boolean(0) // false
Boolean("") // false
Boolean(null) // false
Boolean(undefined) // false
Boolean(NaN) // false
//
// These surprise people
Boolean("0") // true : "0" is a non-empty string!
Boolean("false") // true : "false" is a non-empty string!
Boolean([]) // true : arrays are truthy
Boolean({}) // true : objects are truthy
Boolean(-1) // true : any non-zero number is truthy
Boolean(Infinity) // true : even infinity is truthyBoolean("0") being true is the one that most beginners don't expect. The string "0" contains a character: the digit zero. It is not empty. Length-of-one strings are truthy, regardless of which character they contain.
Parsing Numbers Out of Text: parseInt and parseFloat
Number() is strict. If there's anything in the string that isn't a valid number, the whole thing fails and you get NaN.
Number("42px") // NaN (that "px" killed it)
Number("$100") // NaN (dollar sign killed it)
Number("3.14 kg") // NaN (the " kg" killed it)But real-world data is messy. CSS values have units attached (like "16px"). API responses might give you weights with labels (like "3.14 kg"). User input might have accidental spaces or symbols. This is where parseInt() and parseFloat() come in: they're more lenient extractors.
They start reading from the very first character and keep going until they hit something that isn't a valid part of a number. Then, they stop and return what they found so far.
// parseInt reads the leading integer and stops
parseInt("42px"); // 42 (stopped at "p")
parseInt("100.9em"); // 100 (stopped at ".", gives integer only)
parseInt("3abc"); // 3
//
// parseFloat does the same but handles decimals
parseFloat("3.14rem"); // 3.14 (stopped at "r")
parseFloat("1.5x"); // 1.5
parseFloat("100"); // 100
//
// Both require the string to START with a valid digit or sign
parseInt("px42"); // NaN (started with "p", not a digit)
parseFloat(".5em"); // 0.5 (leading dot is valid for a decimal)The key difference between parseInt and parseFloat is how they treat decimals:
parseIntgives you a whole number (integer). It throws away any decimal part.parseFloatgives you a decimal number. It preserves the decimal portion.
parseInt("3.99") // 3 (decimal dropped, NOT rounded)
parseFloat("3.99") // 3.99 (preserved)A practical example: if you're reading a font size from a CSS property and getting back "16px", you'd use parseInt("16px") to extract the number 16. Number("16px") would give you NaN and your code would break.
The Shorthand Tricks You'll See in Real Code
Open any open-source JavaScript project and you'll see these patterns. They're not doing anything magical: they're just shorter ways to write explicit conversions.
Unary +: Quick Number Conversion
Putting a + directly in front of a value (with no other value on the left side) converts it to a number. This is called the "unary plus": unary meaning "applies to one thing."
+"42" // 42
+"3.14" // 3.14
+true // 1
+false // 0
+null // 0
+undefined // NaN
+"" // 0
+"hello" // NaN
+[] // 0 (empty array → "" → 0)This is equivalent to Number() in every case. Some developers prefer it because it's two characters instead of nine. You'll see it in real code, so you need to recognize it, but for your own code while learning, Number() is clearer and easier to read.
!!: Quick Boolean Conversion
Two exclamation marks in a row convert any value to its boolean equivalent. The first ! converts the value to a boolean and then flips it (so truthy becomes false and falsy becomes true). The second ! flips it back. The result: the correct boolean for that value.
!!"hello" // true (!false, flipped back)
!!"" // false (!true, flipped back)
!!0 // false
!!1 // true
!!null // false
!!undefined // false
!!"0" // true (non-empty string)
!![] // true (array is truthy)You'll see this pattern when developers want to explicitly convert something to a boolean for storage or for a function that specifically needs true or false.
Template Literals: Quick String Conversion
Wrapping a value in a template literal (the backtick syntax) automatically converts it to a string.
`${42}` // "42"
`${true}` // "true"
`${null}` // "null"
`${undefined}` // "undefined"
`${NaN}` // "NaN"
`${[1, 2, 3]}` // "1,2,3"You've probably already used template literals to embed variables in text:
const name = "Alice";
const age = 30;
console.log(`My name is ${name} and I am ${age} years old.`);
// "My name is Alice and I am 30 years old."Every value inside ${} gets converted to a string automatically. So if you ever just need to turn a value into a string quickly, you can wrap it: ${value}.
The Rule That Will Save You More Than Once
Always Use Triple Equals (and Here's Why)
JavaScript has two equality operators, and this is where a lot of the coercion chaos lives.
The == operator (double equals) is called loose equality. Before comparing two values, it performs type coercion. If the types don't match, it converts one or both of them to the same type and then compares. This makes it very hard to predict:
// == coerces before comparing
"5" == 5 // true (string converted to number)
0 == false // true (false converted to 0)
0 == "" // true (both become 0)
null == undefined // true (special case rule)
[] == false // true (that whole chain we saw earlier)The === operator (triple equals) is called strict equality. It never coerces. If the types don't match, it immediately returns false. No conversion, no guessing, no chain of rules to follow.
// === never coerces
"5" === 5 // false (different types, done)
0 === false // false (different types, done)
0 === "" // false (different types, done)
[] === false // false (different types, done)The rule for beginners is simple: use === by default, always. It does exactly what you expect. It won't silently coerce your values and give you a result you didn't intend.
There's one == pattern that experienced developers do use intentionally:
// This checks for BOTH null AND undefined in one shot
if (value == null) {
// runs if value is null OR undefined
// much shorter than: value === null || value === undefined
}null == undefined returns true with loose equality. Neither equals anything else with ==. So this pattern is a clean shortcut when you want to catch both cases together. But that's the only == you need to know right now.
The full story of == (with all its conversion rules) is a whole article on its own, which is exactly what we'll tackle next when we cover equality operators in depth. For now, === is your default, every time.
The Mental Shift
You came into this article thinking JavaScript randomly changes your data. You're leaving knowing that nothing about it is random.
Coercion follows rules. Specific, learnable, completely non-random rules. The + operator prefers strings. The other math operators only do numbers. Seven values are falsy and everything else is truthy. An empty array is truthy because it exists as an object in memory, not because of what's inside it. And when you need to convert something, you call Number(), String(), or Boolean() yourself so you know exactly what happened.
There's a specific bug (the "I got '105' instead of 15" form input bug) that I'd bet a decent amount of money you now understand better than 90% of people who've been writing JavaScript for six months. Not because they're bad developers. Just because nobody explained this to them properly.
Welcome to the other side of that confusion. It's a decent place to be.