Technology  /  JavaScript

๐ŸŸจ JavaScript 15 guides ยท updated 2026

Closures, the event loop, promises, and modern ES syntax โ€” the language of the web explained the way it actually behaves at runtime.

JavaScript Data Types and Coercion: ==, ===, Truthiness, and NaN Explained

JavaScriptโ€™s type system has a terrible reputation, mostly earned from conference-talk screenshots: [] + {} is "[object Object]", "5" - 1 is 4, NaN !== NaN. It looks lawless.

It isnโ€™t. Coercion follows a small, fixed rulebook โ€” weird in places, but mechanical. This guide teaches the rulebook: the eight types, the null/undefined split, truthiness, why === is the default and == is legacy, and the handful of coercion rules that let you predict the screenshots instead of being ambushed by them.


The Eight Types

JavaScript has exactly seven primitive types plus objects:

TypeValuesNotes
number42, 3.14, NaN, Infinityone numeric type โ€” all floating point
string"hi", 'hi', `hi`immutable, UTF-16
booleantrue, false
undefinedundefinedโ€no value was ever setโ€
nullnullโ€intentionally emptyโ€
bigint42nintegers beyond 2โตยณ; niche but real
symbolSymbol("id")unique keys; mostly library/meta territory
object{}, [], functions, datesโ€ฆeverything else โ€” including arrays and functions

Primitives are immutable values compared by value; objects are mutable and compared by reference:

"abc" === "abc" // true โ€” same value
{ a: 1 } === { a: 1 } // false โ€” two different objects!
const x = { a: 1 }, y = x;
x === y // true โ€” same reference

That object-equality behavior surprises everyone once: there is no built-in deep equality for objects. Two structurally identical objects are not ===. Comparing contents means comparing fields yourself, using a library, or (crudely) JSON.stringify both โ€” with its ordering caveats.

Notable details in the table: numbers are all doubles โ€” there is no int type. Integer math is exact up to Number.MAX_SAFE_INTEGER (2โตยณ โˆ’ 1 โ‰ˆ 9 quadrillion); beyond that you need BigInt (this bites real systems that put database IDs through JS math). And yes, 0.1 + 0.2 === 0.30000000000000004 โ€” thatโ€™s IEEE 754 floating point, identical in Java and Python; itโ€™s not a JS quirk, just the one JS gets screenshot-blamed for. Money: work in integer minor units (paise/cents).

typeof is your runtime type probe, with two famous outputs to memorize:

typeof 42 // "number"
typeof "hi" // "string"
typeof undefined // "undefined"
typeof null // "object" โ† the unfixable 1995 bug โ€” null is NOT an object
typeof [] // "object" โ† arrays are objects; use Array.isArray(x)
typeof (() => {}) // "function" โ† the one non-primitive typeof distinguishes

null vs. undefined: One Distinction, Actually Useful

Two empty values, one convention that makes them coexist sanely:

let pending; // undefined โ€” never set
const user = { name: "Ada" };
user.email // undefined โ€” no such property
const match = s.match(/x/); // null โ€” the API says "searched, found nothing"

In your own code: read undefined as โ€œnobody set this,โ€ write null when you mean โ€œexplicitly empty.โ€ And when checking for either, modern JS gives you precision tools:

value ?? fallback // nullish coalescing: fallback ONLY for null/undefined
user?.address?.city // optional chaining: short-circuits to undefined at first null/undefined
count ?? 10 // 0 stays 0! (count || 10 would wrongly replace 0)

That last line is the practical difference between ?? and ||: || replaces any falsy value (including 0, "", false โ€” often legitimate data); ?? replaces only true absence. Since ES2020, ?? + ?. have quietly eliminated an entire genre of defensive-checking boilerplate. Use them.


Truthiness: The Whole List

Conditions in JS accept any value, coercing it to boolean. The falsy list is short enough to memorize completely โ€” everything not on this list is truthy:

false 0 -0 0n "" null undefined NaN

Which means these are all truthy, and each has caught someone:

if ("false") { } // truthy โ€” non-empty string
if ([]) { } // truthy โ€” empty array is an object
if ({}) { } // truthy โ€” empty object too
if ("0") { } // truthy โ€” non-empty string!

Truthiness is genuinely convenient โ€” if (items.length), if (errorMessage) โ€” but it has a classic failure mode: treating legitimate falsy data (0, empty string) as absence. A quantity of 0 or a middle name of "" is data, not nothing:

// bug: user typed 0, code thinks "not provided"
const qty = input || 1;
// fix:
const qty = input ?? 1;

The rule of thumb: truthiness for โ€œdoes this collection/string have anythingโ€ checks; explicit comparisons (=== null, !== undefined, ??) whenever 0/""/false are valid values in your domain.


Coercion: The Rulebook Behind the Screenshots

Implicit conversion happens in three contexts, each with fixed rules:

To string โ€” the + operator, when either side is a string:

"5" + 1 // "51" โ€” + prefers concatenation
1 + 2 + "3" // "33" โ€” left-to-right: 3, then "33"
"" + 42 // "42" โ€” old-school numberโ†’string trick

To number โ€” every other arithmetic operator, comparisons with </>, and unary +:

"5" - 1 // 4 โ€” minus has no string meaning, so both sides โ†’ number
"5" * "2" // 10
+"42" // 42 โ€” unary plus: terse stringโ†’number conversion
"abc" - 1 // NaN โ€” "abc" can't become a number
true + 1 // 2 โ€” true โ†’ 1, false โ†’ 0

So the infamous asymmetry โ€” "5" + 1 vs "5" - 1 โ€” reduces to one sentence: + means concat if any string is present; every other math operator means numbers. Most viral โ€œJS is brokenโ€ examples are this rule plus the object-to-primitive rule (objects become strings like "[object Object]", arrays join their elements: [1,2] + "" is "1,2"), applied consistently.

Explicit conversion โ€” prefer it in real code; it documents intent:

Number("42") // 42
Number("") // 0 โ† surprise worth knowing
Number(null) // 0 โ† and this
Number(undefined) // NaN
parseInt("42px") // 42 โ€” parses a leading number, ignores the rest
parseFloat("3.9m") // 3.9
String(42) // "42"
Boolean(x) // truthiness, made explicit (same as !!x)

Number converts the whole string strictly; parseInt/parseFloat scavenge a leading number โ€” pick by whether trailing junk should fail or be ignored. For user input, remember both accept garbage differently: validate before trusting.


NaN: The Value That Isnโ€™t Equal to Itself

NaN (โ€œnot a numberโ€ โ€” which is, of course, of type number) results from failed numeric operations: Number("abc"), 0/0, Math.sqrt(-1). Its defining property comes from the IEEE floating-point standard:

NaN === NaN // false โ€” NaN is not equal to ANYTHING, including itself
Number.isNaN(NaN) // true โ€” the correct test
Number.isNaN("abc") // false โ€” checks "is this actually the NaN value"
isNaN("abc") // true โ€” legacy global: coerces first, misleading; avoid

The practical hazards: NaN silently propagates (any arithmetic touching NaN yields NaN), so one bad parse can turn an entire calculation chain into NaN by the time you notice โ€” and since NaN is falsy and unequal to everything, it slips through naive checks. Defenses: validate inputs at the boundary (Number.isFinite(x) is often the right gate โ€” it rejects NaN and Infinity and non-numbers), and when a NaN appears, trace backwards to the earliest operation that could have produced it.


== vs ===: End of Debate

=== (strict equality) compares without coercion: different types โ†’ false, done. == (loose equality) coerces first, following an arcane table:

1 === "1" // false
1 == "1" // true โ€” string coerced to number
0 == false // true
"" == 0 // true
null == undefined // true โ€” and == with null matches ONLY undefined
null == 0 // false โ† inconsistent with the previous lines' logic
NaN == NaN // false โ€” nothing helps NaN

The verdict, shared by every style guide, linter default, and this series: always === and !==. The single defensible use of == youโ€™ll meet in the wild is x == null as an intentional shorthand for โ€œnull or undefinedโ€ โ€” recognize it in code review, and prefer x === null || x === undefined or restructuring with ?? when writing.

Comparison has one more trap worth flagging: relational operators on mixed types. "10" < "9" is true (string comparison is lexicographic), while "10" < 9 is false (number coercion). If either side might be a string โ€” user input, URL params, form fields โ€” convert explicitly before comparing. Sorting has the same disease: [10, 9, 1].sort() gives [1, 10, 9] because default sort is stringwise; you want sort((a, b) => a - b), detailed in array methods.


Frequently Asked Questions

Why does JavaScript even have coercion? The 1995 design goal: scripts embedded in pages, written by non-programmers, should limp onward rather than crash. Form fields deliver strings; coercion made "5" * quantity work. In 2026 we have better tools (explicit conversion, TypeScript), but the donโ€™t-break-the-web rule means the behavior is permanent โ€” hence the strategy of knowing it while avoiding relying on it.

Does TypeScript fix all of this? Much of it: TS flags mixed-type comparisons and arithmetic at compile time, which is a big part of its value. But TS types vanish at runtime โ€” data arriving from APIs, JSON, and users is whatever it is, so boundary validation (and understanding these rules) remains your job.

How do I check types reliably? typeof for primitives (minus the null bug), Array.isArray for arrays, x === null for null, Number.isFinite/Number.isNaN for numeric health, instanceof for class instances (with caveats across frames/realms). There is no one universal type check โ€” thatโ€™s honest, if annoying.

Whatโ€™s the deal with Object.is? A third equality: like === except Object.is(NaN, NaN) is true and it distinguishes +0/-0. Rarely needed directly, but itโ€™s what Array.prototype.includes uses โ€” which is why [NaN].includes(NaN) is true while [NaN].indexOf(NaN) is -1. Good trivia; better reason to use includes.


A Boundary-Validation Habit That Replaces Half of This Chapter

The deepest defense against coercion bugs isnโ€™t memorizing rules โ€” itโ€™s ensuring types are known everywhere except the edges. Data enters JS programs from exactly a few doors: user input (always strings), URL params (strings), JSON APIs (whatever the server sent), storage (strings via localStorage). Convert and validate at the door, once:

function parseQuantity(raw) {
const n = Number(raw);
if (!Number.isInteger(n) || n < 1 || n > 999) {
throw new RangeError(`Quantity must be 1-999, got "${raw}"`);
}
return n;
}

Past that function, quantity is a trustworthy integer โ€” no downstream parseInt sprinkles, no || 1 guards, no wondering whether qty + 1 will concatenate. Every conversion rule in this chapter still applies inside the boundary function; it just applies in one place instead of fifty. This is the pattern TypeScript formalizes (types trusted inside, validation at the perimeter), and adopting it in plain JS makes the eventual TS migration nearly mechanical.

Self-Check

Predict each โ€” the rulebook covers all of them:

1. "10" - 5
2. "10" + 5
3. [] + ""
4. 0 ?? 7
5. 0 || 7
6. Number("")
7. "b" + "a" + +"a" + "s" // a classic โ€” hint: unary plus
8. [3, 20, 100].sort()[0]
9. NaN === NaN
10. typeof null

Answers: (1) 5 โ€” minus means numbers. (2) "105" โ€” plus with a string concatenates. (3) "" โ€” array coerces to empty string. (4) 0 โ€” nullish keeps falsy-but-present values. (5) 7 โ€” || discards 0. (6) 0 โ€” empty string โ†’ 0. (7) "baNaNs" โ€” +"a" is NaN, concatenated. (8) 100 โ€” stringwise sort puts โ€œ100โ€ first. (9) false. (10) "object".

Score eight or more and coercion no longer owns you. Next: Functions โ€” declarations vs arrows, parameters and defaults, and functions-as-values, the idea the entire rest of the language is built on.