Types, equality and coercion
JavaScript's flexibility is both its superpower and its trap. You've probably seen [] + {} produce "[object Object]" and wondered if someone was pranking you. The truth is, understanding types, equality and coercion isn't about memorizing quirks—it's about knowing the rules so well that nothing surprises you anymore.
Seven primitives plus objects
JavaScript has exactly eight fundamental types. Seven are primitives: string, number, bigint, boolean, undefined, symbol, and null. Everything else—functions, arrays, plain objects, dates, errors—is an object. This distinction matters because primitives are immutable values, while objects are references.
Let me show you what each looks like:
// Primitives
const str: string = "hello";
const num: number = 42;
const big: bigint = 42n;
const bool: boolean = true;
const undef: undefined = undefined;
const sym: symbol = Symbol("id");
const nil: null = null;
// Objects
const obj = { name: "Alice" };
const arr = [1, 2, 3];
const fn = () => "result";
console.log(typeof str); // "string"
console.log(typeof num); // "number"
console.log(typeof big); // "bigint"
console.log(typeof bool); // "boolean"
console.log(typeof undef); // "undefined"
console.log(typeof sym); // "symbol"
console.log(typeof nil); // "object" (historical quirk)
console.log(typeof obj); // "object"
console.log(typeof arr); // "object"
console.log(typeof fn); // "function"Notice that typeof null returns "object"—a legacy bug from JavaScript's early days that we're stuck with. To check for null safely, use strict equality: value === null.
Coercion — Operators convert types implicitly
Coercion happens when JavaScript automatically converts one type to another. It's implicit and often surprising, but it follows consistent rules. The main triggers are the + operator (addition or concatenation), comparison operators, and logical operators.
String concatenation with +: If either operand is a string, + concatenates. This is where [] + {} gets interesting:
console.log([] + {}); // "[object Object]"
console.log({} + []); // "[object Object]" (or 0 in some contexts)
console.log("5" + 3); // "53"
console.log(3 + "5"); // "35"
console.log([] + []); // ""
console.log([1, 2] + [3, 4]); // "1,23,4"When you add an object to anything, JavaScript calls toString() on it. Arrays become their comma-separated elements, and plain objects become "[object Object]". Empty array becomes empty string, so [] + [] is truly "" + "".
Numeric coercion with - * / %: These operators always want numbers, so coercion is more predictable:
console.log("5" - 3); // 2
console.log("5" - "3"); // 2
console.log("hello" - 3); // NaN
console.log(true - 1); // 0
console.log(false - 1); // -1
console.log(null - 1); // -1
console.log(undefined - 1); // NaNComparison operators: The == operator performs type coercion; === doesn't. This is where bugs hide:
console.log(0 == false); // true (coerces false to 0)
console.log(0 === false); // false
console.log("" == false); // true (coerces "" to 0, false to 0)
console.log("" === false); // false
console.log(null == undefined); // true (special rule)
console.log(null === undefined); // false
console.log("5" == 5); // true
console.log("5" === 5); // falseIn production code, always use === and !==. The loose equality == creates inconsistency and subtle bugs that are hard to trace.
Falsy values — false, 0, -0, 0n, '', null, undefined and NaN
In conditional contexts (if statements, ternary operators, logical AND/OR), JavaScript coerces values to boolean. There are exactly eight falsy values; everything else is truthy.
const falsyValues = [false, 0, -0, 0n, '', null, undefined, NaN];
falsyValues.forEach(val => {
if (!val) {
console.log(`${typeof val} ${val} is falsy`);
}
});
// Output:
// boolean false is falsy
// number 0 is falsy
// number -0 is falsy
// bigint 0 is falsy
// string is falsy
// object null is falsy
// undefined undefined is falsy
// number NaN is falsyEverything else is truthy, including "0", [], {}, and -1:
if ("0") console.log("string '0' is truthy"); // logs
if ([]) console.log("empty array is truthy"); // logs
if ({}) console.log("empty object is truthy"); // logs
if (-1) console.log("-1 is truthy"); // logs
// This is a common mistake:
const count = 0;
if (count) {
console.log("count exists"); // never runs
} else {
console.log("count doesn't exist or is 0"); // logs
}When you need to distinguish between 0 and null, be explicit about what you're checking.
Be explicit — Convert with Number(), String() and Boolean() deliberately
Rather than relying on implicit coercion, convert types intentionally. This makes your intent clear and prevents bugs.
Number() conversion:
console.log(Number("42")); // 42
console.log(Number("3.14")); // 3.14
console.log(Number("hello")); // NaN
console.log(Number(true)); // 1
console.log(Number(false)); // 0
console.log(Number(null)); // 0
console.log(Number(undefined)); // NaN
console.log(Number("")); // 0
console.log(Number(" 42 ")); // 42 (trims whitespace)
// Use parseInt and parseFloat for more control
console.log(parseInt("42px")); // 42
console.log(parseFloat("3.14em")); // 3.14
console.log(parseInt("hello")); // NaNString() conversion:
console.log(String(42)); // "42"
console.log(String(true)); // "true"
console.log(String(false)); // "false"
console.log(String(null)); // "null"
console.log(String(undefined)); // "undefined"
console.log(String([1, 2, 3])); // "1,2,3"
console.log(String({ id: 1 })); // "[object Object]"
// Use toString() for more control over formatting
const num = 255;
console.log(num.toString(16)); // "ff" (hexadecimal)Boolean() conversion:
console.log(Boolean(1)); // true
console.log(Boolean(0)); // false
console.log(Boolean("")); // false
console.log(Boolean("hello")); // true
console.log(Boolean(null)); // false
console.log(Boolean(undefined)); // false
console.log(Boolean([])); // true
console.log(Boolean({})); // true
// Double negation is another pattern (less readable)
console.log(!!1); // true
console.log(!!0); // falseIn TypeScript, use type predicates and type guards to be even more explicit:
function isValidNumber(value: unknown): value is number {
return typeof value === "number" && !isNaN(value);
}
function isValidString(value: unknown): value is string {
return typeof value === "string" && value.length > 0;
}
const input: unknown = getUserInput();
if (isValidNumber(input)) {
console.log(input + 10); // input is number here
}
if (isValidString(input)) {
console.log(input.toUpperCase()); // input is string here
}Common mistakes
Mistake 1: Trusting == for comparisons
// Bad
if (userInput == null) { }
if (status == 0) { }
// Good
if (userInput === null || userInput === undefined) { }
if (status === 0) { }
// Or use optional chaining and nullish coalescing
if (!userInput) { } // if you want to catch both null and undefinedMistake 2: Forgetting about NaN
const result = Number("hello");
if (result == NaN) { } // Wrong! NaN == NaN is false
if (isNaN(result)) { } // Good
if (Number.isNaN(result)) { } // Better (doesn't coerce)Mistake 3: Assuming 0 means "no value"
// Bad
function setQuantity(qty) {
if (!qty) return; // Skips when qty is 0
}
// Good
function setQuantity(qty) {
if (qty === undefined || qty === null) return;
}
// Or with nullish coalescing
const quantity = userQty ?? 1; // Use 1 only if userQty is null or undefinedMistake 4: Chaining loose equality
// This looks like transitivity but fails
console.log(0 == false); // true
console.log(false == ""); // true
console.log(0 == ""); // true
// But that's not what == does—each pair is evaluated separately
// Always use ===
console.log(0 === false); // false
console.log(false === ""); // false
console.log(0 === ""); // falseKey takeaways
- JavaScript has eight types: seven primitives (string, number, bigint, boolean, undefined, symbol, null) and objects. Know which is which.
- Coercion is implicit type conversion triggered by operators. It follows rules but is often surprising—avoid relying on it.
- Always use strict equality (
===,!==) instead of loose equality (==,!=). - There are exactly eight falsy values:
false,0,-0,0n,"",null,undefined, andNaN. Everything else is truthy. - Convert types explicitly with
Number(),String(), andBoolean()to make your intent clear. - Use TypeScript's type predicates and type guards to enforce type safety and catch coercion issues at compile time.
- The phrase
[] + {}becomes obvious once you understand that arrays coerce to their string representation and objects coerce to"[object Object]"—it's not magic, it's rules.






Comments
Loading comments…