JavaScript Type Coercion: The Complete Guide to Implicit & Explicit Conversion
JavaScript is a dynamically typed language. Variables do not have fixed types, and values can be converted from one type to another — sometimes automatically. This automatic conversion is called type coercion, and it is responsible for more bugs, interview questions, and confused developers than almost any other JavaScript feature. Let us master it completely.
Explicit vs implicit conversion
There are two ways values change type in JavaScript. Explicit conversion (also called type casting) is when you intentionally convert a value using a function or operator. Implicit coercion is when JavaScript automatically converts a value behind the scenes during an operation.
// Explicit conversion — you are in control
const str = String(42); // '42'
const num = Number('42'); // 42
const bool = Boolean(1); // true
// Implicit coercion — JavaScript decides for you
const result = '5' + 3; // '53' (number coerced to string)
const math = '5' - 3; // 2 (string coerced to number)
const check = !!'hello'; // true (string coerced to boolean)
The distinction matters because explicit conversion is predictable and intentional, while implicit coercion follows rules that can surprise you if you do not know them.
Converting to string
Explicit string conversion
// String() constructor — the most reliable way
String(42) // '42'
String(true) // 'true'
String(false) // 'false'
String(null) // 'null'
String(undefined) // 'undefined'
String(NaN) // 'NaN'
String([1, 2, 3]) // '1,2,3'
String({}) // '[object Object]'
String(Symbol('x')) // 'Symbol(x)'
// .toString() method — works on most values
(42).toString() // '42'
true.toString() // 'true'
[1, 2].toString() // '1,2'
// But null and undefined don't have .toString()
// null.toString() // TypeError!
// undefined.toString() // TypeError!
// Template literals — cleanest approach in modern code
const age = 25;
const str = \`\${age}\`; // '25'
Implicit string coercion
The + operator triggers string coercion when one operand is a string. This is the single most common source of coercion bugs.
// The + operator with a string triggers concatenation
'5' + 3 // '53' — 3 becomes '3'
'5' + true // '5true'
'5' + null // '5null'
'5' + undefined // '5undefined'
'5' + [1, 2] // '51,2'
'5' + {} // '5[object Object]'
// Order matters
3 + 4 + '5' // '75' — (3+4) = 7, then 7 + '5' = '75'
'5' + 3 + 4 // '534' — '5' + 3 = '53', then '53' + 4 = '534'
// This is why you see bugs like:
console.log('Total: ' + 10 + 20); // 'Total: 1020' (not 'Total: 30')
console.log('Total: ' + (10 + 20)); // 'Total: 30' (parentheses fix it)
Converting to number
Explicit number conversion
// Number() — the standard way
Number('42') // 42
Number('3.14') // 3.14
Number('') // 0 (empty string = 0)
Number(' ') // 0 (whitespace = 0)
Number('hello') // NaN
Number(true) // 1
Number(false) // 0
Number(null) // 0
Number(undefined) // NaN (not 0!)
Number([]) // 0
Number([5]) // 5
Number([1, 2]) // NaN
// parseInt and parseFloat — more forgiving parsers
parseInt('42px') // 42 (stops at non-numeric character)
parseInt('0xFF') // 255 (hex)
parseInt('0b1010') // 0 (binary NOT supported by parseInt)
parseInt('abc') // NaN
parseFloat('3.14m') // 3.14
parseFloat('.5') // 0.5
// parseInt with radix (always specify it!)
parseInt('10', 2) // 2 (binary)
parseInt('10', 8) // 8 (octal)
parseInt('10', 16) // 16 (hex)
parseInt('10', 10) // 10 (decimal — always use this for safety)
// Unary + operator — shortest syntax
+'42' // 42
+'' // 0
+true // 1
+false // 0
+null // 0
+undefined // NaN
+[] // 0
+[5] // 5
Implicit number coercion
Most math operators (except +) trigger number coercion. Comparison operators also coerce values to numbers.
// Arithmetic operators (except +) coerce to number
'6' - 2 // 4
'6' * 2 // 12
'6' / 2 // 3
'6' % 2 // 0
'6' ** 2 // 36
true + true // 2 (1 + 1)
false + 1 // 1 (0 + 1)
null + 5 // 5 (0 + 5)
// Comparison operators coerce to number
'5' > 3 // true (5 > 3)
'01' == 1 // true (1 == 1)
true == 1 // true (1 == 1)
false == 0 // true (0 == 0)
null == 0 // false! (special rule)
'' == 0 // true (0 == 0)
Converting to boolean
Truthy and falsy values
This is one of the most important concepts in JavaScript. Every value is either truthy or falsy. There are exactly eight falsy values — everything else is truthy.
// The 8 FALSY values (memorize these)
Boolean(false) // false
Boolean(0) // false
Boolean(-0) // false
Boolean(0n) // false (BigInt zero)
Boolean('') // false (empty string)
Boolean(null) // false
Boolean(undefined) // false
Boolean(NaN) // false
// EVERYTHING else is truthy, including these surprises:
Boolean('0') // true (non-empty string!)
Boolean(' ') // true (whitespace is truthy)
Boolean('false') // true (the string 'false' is truthy!)
Boolean([]) // true (empty array is truthy!)
Boolean({}) // true (empty object is truthy!)
Boolean(-1) // true (negative numbers are truthy)
Boolean(Infinity) // true
Boolean(new Date()) // true
Boolean(function(){}) // true
Implicit boolean coercion
Boolean coercion happens in conditions, logical operators, and the ternary operator.
// if statements coerce to boolean
if ('hello') { /* runs — truthy */ }
if (0) { /* skipped — falsy */ }
if ([]) { /* runs — empty array is truthy! */ }
if (null) { /* skipped — falsy */ }
// Logical operators
const name = '' || 'Anonymous'; // 'Anonymous' ('' is falsy)
const count = 0 || 10; // 10 (0 is falsy)
const value = null ?? 'default'; // 'default' (?? checks null/undefined only)
const zero = 0 ?? 10; // 0 (?? does NOT treat 0 as missing)
// Double NOT for explicit boolean conversion
!!'' // false
!!'hello' // true
!!0 // false
!!42 // true
!!null // false
!!undefined // false
!![] // true (remember: empty array is truthy)
The equality nightmare: == vs ===
The == operator (loose equality) performs type coercion before comparing. The === operator (strict equality) does not — it requires both type and value to match. This is why every style guide says: always use ===.
// === strict equality — no coercion, no surprises
5 === 5 // true
5 === '5' // false (different types)
0 === false // false (different types)
null === undefined // false (different types)
NaN === NaN // false (NaN is never equal to anything, including itself)
// == loose equality — coercion happens
5 == '5' // true ('5' → 5)
0 == false // true (false → 0)
0 == '' // true ('' → 0)
'' == false // true (both → 0)
null == undefined // true (special rule!)
null == 0 // false (another special rule!)
null == '' // false
// The classic gotchas
[] == false // true ([] → '' → 0, false → 0)
[] == 0 // true
[] == '' // true
[1] == 1 // true
'' == 0 // true
' \t\n' == 0 // true (whitespace string → 0)
// The absurd chain
'' == 0 // true
0 == '0' // true
'' == '0' // false! (both strings, compared as strings)
// Object.is() — the truly correct equality check
Object.is(NaN, NaN) // true (fixes the NaN problem)
Object.is(0, -0) // false (distinguishes +0 and -0)
Object.is(5, 5) // true
Object to primitive coercion
When objects need to become primitives (for math, comparison, or string concatenation), JavaScript calls internal methods in a specific order.
// Objects have Symbol.toPrimitive, valueOf, and toString methods
// JavaScript calls them in this priority order:
// 1. Symbol.toPrimitive (if defined)
const custom = {
[Symbol.toPrimitive](hint) {
if (hint === 'number') return 42;
if (hint === 'string') return 'forty-two';
return true; // hint === 'default'
}
};
+custom // 42 (hint: 'number')
\`\${custom}\` // 'forty-two' (hint: 'string')
custom + '' // 'true' (hint: 'default')
// 2. valueOf() — called for numeric context
const obj = {
valueOf() { return 10; },
toString() { return 'ten'; }
};
obj + 5 // 15 (valueOf → 10)
\`\${obj}\` // 'ten' (toString for string hint)
// 3. toString() — called for string context
const arr = [1, 2, 3];
arr + '' // '1,2,3' (toString)
+arr // NaN (valueOf returns the array itself, then toString → '1,2,3' → NaN)
// Date is special — prefers string conversion
const date = new Date();
date + 1 // 'Wed May 07 2026 ...1' (toString, not valueOf)
+date // 1778342400000 (explicit + calls valueOf → timestamp)
Common coercion traps and how to avoid them
// Trap 1: String concatenation with +
function add(a, b) {
return a + b; // If either is a string, you get concatenation!
}
add(5, '3') // '53' — not 8!
// Fix: explicitly convert
function addSafe(a, b) {
return Number(a) + Number(b);
}
addSafe(5, '3') // 8
// Trap 2: Checking for empty arrays
if ([]) { console.log('runs!'); } // Empty array is truthy!
if ([].length) { console.log('nope'); } // Check .length instead
// Trap 3: Comparing with null
null == undefined // true
null == 0 // false
null == '' // false
null == false // false
// Only null == undefined is true. null is not == to anything else.
// Trap 4: parseInt surprises
parseInt(0.000001) // 0 (reads '0.000001')
parseInt(0.0000001) // 1 (reads '1e-7' → stops at '1')!
parseInt(1/0) // NaN (reads 'Infinity')
parseInt(Infinity) // NaN
// Trap 5: JSON.stringify quirks
JSON.stringify(undefined) // undefined (not a string!)
JSON.stringify(NaN) // 'null'
JSON.stringify(Infinity) // 'null'
JSON.stringify({a: undefined, b: 1}) // '{"b":1}' (undefined props removed)
Type checking best practices
// typeof — good for primitives
typeof 'hello' // 'string'
typeof 42 // 'number'
typeof true // 'boolean'
typeof undefined // 'undefined'
typeof null // 'object' (famous bug, never fixed!)
typeof [] // 'object' (arrays are objects)
typeof {} // 'object'
typeof function(){} // 'function'
typeof Symbol() // 'symbol'
typeof 42n // 'bigint'
// Better checks for specific types
Array.isArray([]) // true
Array.isArray({}) // false
Number.isNaN(NaN) // true
Number.isNaN('hello') // false (unlike global isNaN)
Number.isFinite(42) // true
Number.isFinite(Infinity) // false
Number.isInteger(5) // true
Number.isInteger(5.0) // true (!)
Number.isInteger(5.5) // false
// Check for null explicitly
if (value === null) { /* null */ }
// Check for undefined
if (typeof value === 'undefined') { /* undefined */ }
// Check for null OR undefined
if (value == null) { /* null or undefined — one of the few good uses of == */ }
// instanceof for objects
[] instanceof Array // true
{} instanceof Object // true (but everything is!)
new Date() instanceof Date // true
The golden rules of type coercion
After all these rules, here is what every JavaScript developer should do:
// 1. Always use === instead of ==
// Exception: value == null (checks for null or undefined)
// 2. Be explicit about conversions
const num = Number(input); // not +input
const str = String(value); // not '' + value
const bool = Boolean(value); // not !!value (though !! is acceptable)
// 3. Validate input types
function calculateTax(price, rate) {
if (typeof price !== 'number' || typeof rate !== 'number') {
throw new TypeError('Arguments must be numbers');
}
return price * rate;
}
// 4. Use Number.isNaN instead of global isNaN
isNaN('hello') // true (coerces to NaN first — misleading)
Number.isNaN('hello') // false (does not coerce — correct)
Number.isNaN(NaN) // true
// 5. Use nullish coalescing (??) instead of || for defaults
const port = config.port ?? 3000; // only replaces null/undefined
const port2 = config.port || 3000; // also replaces 0, '', false
Type coercion is not a flaw in JavaScript — it is a design choice that trades safety for flexibility. By understanding the rules, you turn coercion from a source of bugs into a tool you can use confidently. The key is being intentional: know when coercion happens, and make your conversions explicit when the implicit rules are not obvious.