JavaScript data types and type coercion
JavaScript's type system is small but full of sharp edges: seven primitives, one
object type, and a coercion engine that quietly converts values between types. That
coercion powers the "wat" examples ([] == ![], 0.1 + 0.2 !== 0.3) and a surprising
share of real bugs. This guide covers the types, equality, coercion rules, truthiness,
and the gotchas worth knowing cold.
The types
There are seven primitive types — string, number, boolean, null,
undefined, symbol, bigint — and everything else is an object (including
arrays and functions).
typeof 'hi' // 'string'
typeof 42 // 'number'
typeof 10n // 'bigint'
typeof Symbol() // 'symbol'
typeof undefined // 'undefined'
typeof null // 'object' <- historical bug; null is a primitive
typeof [] // 'object' <- arrays aren't singled out
typeof function(){} // 'function' <- functions are
Primitives are immutable and compared by value; objects are mutable and
compared by reference. When you call a method on a primitive ('hi'.toUpperCase()),
JavaScript temporarily boxes it in a wrapper object, then discards it — which is why
you should never use new Number/new String explicitly (the result is an object,
breaks ===, and is always truthy).
null vs undefined
Both mean "no value," but with different intent: undefined is the engine's default
absence (an unassigned variable, a missing property, a missing return); null is an
intentional "deliberately empty" you assign.
let a // undefined
const obj = {}
obj.missing // undefined
const b = null // null
null == undefined // true (loose)
null === undefined // false (different types)
typeof null is the famous 'object' bug, kept for compatibility.
Equality: == vs ===
=== (strict) compares value and type with no conversion. == (loose) coerces
the operands first, producing surprising results.
0 === '' // false
0 == '' // true (both coerce to 0)
0 == '0' // true
'' == '0' // false (not even transitive!)
NaN == NaN // false
[] == ![] // true ([]->'', ![]->false->0, ''->0)
The == algorithm, simplified: same type -> compare directly; null == undefined is a
special true; number vs string -> string->number; boolean -> number; object vs primitive
-> convert the object to a primitive, then retry. Prefer === almost always. The one
common idiom is x == null, which matches both null and undefined. For the NaN/
-0 edge cases, Object.is is like === but treats NaN as equal to itself and +0
distinct from -0.
Coercion
Coercion is automatic type conversion, implicit ('5' * 2) or explicit
(Number('5')). The operator that trips everyone up is +: if either side is a string
it does string concatenation; the other arithmetic operators force numbers.
1 + '2' // '12' (+ with a string -> concat)
1 - '2' // -1 (- forces numeric)
'5' * '2' // 10
true + 1 // 2 (true -> 1)
[] + {} // '[object Object]'
Objects coerce via Symbol.toPrimitive, then valueOf (number hint) or toString
(string hint). Arrays coerce to a comma-joined string, so Number([]) is 0,
Number([5]) is 5, and Number([1,2]) is NaN — the source of the [] == ![]
puzzles. Template literals always coerce to string, which is why ${obj} yields
[object Object] unless you override toString or interpolate a specific field.
Truthy and falsy
There are exactly eight falsy values; everything else is truthy.
false, 0, -0, 0n, '', null, undefined, NaN // the only falsy values
The traps are values people expect to be falsy but aren't: '0', 'false', [],
and {} are all truthy.
if ([]) console.log('runs!') // empty array is truthy
Boolean('0') // true
This matters for conditions, ||/&&, and the nullish coalescing operator ??,
which (unlike ||) only falls back on null/undefined, not on 0 or '':
const count = 0
count || 10 // 10 treats valid 0 as missing
count ?? 10 // 0
Pair it with optional chaining ?., which short-circuits to undefined instead of
throwing: user?.address?.city ?? 'unknown'.
Numbers, NaN, and BigInt
All JS numbers are 64-bit IEEE-754 doubles, so decimal fractions are approximate:
0.1 + 0.2 // 0.30000000000000004
0.1 + 0.2 === 0.3 // false
Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON // true — compare with a tolerance
For money, use integer cents or a decimal library. NaN ("Not-a-Number") is the only
value not equal to itself, so detect it with Number.isNaN (not the global
isNaN, which coerces). NaN is contagious — any arithmetic with it yields NaN. For
integers beyond 2^53 - 1, use BigInt (10n), but you can't mix BigInt and
Number in arithmetic.
Type checking
typeof x === 'function' // reliable for callables
Array.isArray(x) // the ONLY reliable array check (typeof [] is 'object')
x === null // explicit null check
Number.isNaN(x) // NaN check
Array.isArray beats instanceof Array, which breaks across realms (an array from an
<iframe> has a different constructor). And typeof is special-cased to not throw
for undeclared identifiers, making it safe for feature detection (typeof window !== 'undefined') — though it still throws for let/const in the temporal dead zone.
Recap
JavaScript has seven immutable, value-compared primitives and one reference-compared
object type. Coercion converts between them — + prefers strings, other
operators prefer numbers, objects go through valueOf/toString. Prefer === to
avoid =='s surprises, memorize the eight falsy values, and reach for ??/?. for
safe defaults and access. Mind floating-point precision, detect NaN with
Number.isNaN, and check arrays with Array.isArray. Know these rules and the
coercion brain-teasers become straightforward derivations.