Skip to content
Plurafy

Type Coercion and Equality

Understanding JavaScript's type system and comparison operators

Why do these comparisons produce unexpected results?

console.log(0 == false); // true or false?
console.log(0 === false); // true or false?
console.log('' == false); // true or false?
console.log(null == undefined); // true or false?
console.log([1] == 1); // true or false?

JavaScript performs automatic type conversion (coercion) in certain contexts. Understanding strict (===) vs loose (==) equality is critical for avoiding bugs.

// == performs type coercion
console.log(0 == false); // true - false converts to 0
console.log(1 == true); // true - true converts to 1
console.log('' == false); // true - '' converts to 0, false converts to 0
console.log('5' == 5); // true - '5' converts to 5
console.log(null == undefined); // true - special case
console.log([1] == 1); // true - [1] converts to 1
// Coercion rules:
// 1. If types are different, coerce to common type
// 2. Object to primitive: call valueOf() then toString()
// 3. String to number: parse string as number
// === No type coercion, must be same type
console.log(0 === false); // false - different types
console.log(1 === true); // false - different types
console.log('5' === 5); // false - different types
console.log(null === undefined); // false - different types
console.log([] === []); // false - different object instances
console.log(NaN === NaN); // false - NaN special case!
// Use Object.is() for strict comparison including NaN
console.log(Object.is(NaN, NaN)); // true
console.log(Object.is(-0, +0)); // false (unlike ===)
// ✅ Best Practice: Always use ===
function compareValues(a, b) {
return a === b; // Explicit, predictable
}
// ✅ For NaN checking
const value = NaN;
console.log(Number.isNaN(value)); // true
console.log(Object.is(value, NaN)); // true
// ✅ Nullish coalescing operator (prefer over ==)
const username = null;
const defaultName = username ?? 'Guest'; // Only null/undefined
// ❌ Avoid loose equality with nullish values
const value = null;
console.log(value == undefined); // true (but values differ!)
console.log(value === undefined); // false (correct)
// In arithmetic operations
console.log('5' + 3); // '53' - string concatenation
console.log('5' - 3); // 2 - numeric subtraction
console.log('5' * '2'); // 10 - numeric multiplication
console.log('5' / '2'); // 2.5 - numeric division
console.log(true + 1); // 2 - true converts to 1
console.log(false + 1); // 1 - false converts to 0
// In boolean context
console.log(!!'hello'); // true - truthy value
console.log(!!0); // false - falsy value
console.log(!!null); // false - falsy value
console.log(!!undefined); // false - falsy value
console.log(!!''); // false - falsy value
console.log(!!NaN); // false - falsy value
console.log(!!-1); // true - all other numbers truthy
const falsy = [
false,
0,
-0,
0n, // BigInt
'', // empty string
null,
undefined,
NaN
];
const truthy = [
true,
1,
-1,
'0', // non-empty string (even '0')
' ', // whitespace
[], // empty array
{}, // empty object
() => {} // function
];
// Practical: Checking for values
function isValid(value) {
return value != null; // Handles null and undefined
}
console.log(isValid(0)); // true
console.log(isValid('')); // true
console.log(isValid(false)); // true
console.log(isValid(null)); // false
console.log(isValid(undefined)); // false
// When object is coerced to primitive, JavaScript calls:
// 1. valueOf() - should return primitive
// 2. toString() - fallback
const custom = {
value: 42,
valueOf() {
return this.value;
},
toString() {
return `Custom(${this.value})`;
}
};
console.log(custom + 10); // 52 (valueOf called)
console.log(String(custom)); // 'Custom(42)' (toString called)
console.log(`Value: ${custom}`); // 'Value: Custom(42)'
// Array to primitive
const arr = [1, 2, 3];
console.log(arr + 0); // '1,2,30' - arr.toString() = '1,2,3'
console.log(String(arr)); // '1,2,3'
console.log(Number(arr)); // NaN - can't convert
const single = [42];
console.log(Number(single)); // 42 - special case
// ⚠️ Confusing loose equality
if (value == true) { } // Dangerous!
if (value) { } // Better - boolean context
// ⚠️ String concatenation
const result = 1 + 2 + '3'; // '33' not '6'
const better = 1 + 2 + Number('3'); // 6
// ⚠️ Array comparison
const arr = [1, 2];
if (arr == [1, 2]) { } // false! Different objects
if (arr.toString() === [1, 2].toString()) { } // true
// ⚠️ setTimeout callback
setTimeout((x = 1) => { // x is parameter, not coerced
console.log(x);
}, 100, true); // true passed as argument, becomes 1
// Checking types safely
function checkType(value) {
// typeof for primitives
console.log(typeof value);
// instanceof for objects
console.log(value instanceof Array);
// Object.prototype.toString for accurate type
console.log(Object.prototype.toString.call(value));
// Number check (handles NaN)
console.log(Number.isFinite(value));
// Array check (modern)
console.log(Array.isArray(value));
// Null check
console.log(value === null);
}
// Safe type coercion
function safeToNumber(value) {
const num = Number(value);
return Number.isFinite(num) ? num : 0;
}
function safeToString(value) {
return value == null ? '' : String(value);
}
function safeToBoolean(value) {
return Boolean(value);
}