Skip to content

JavaScript · Fundamentals

JavaScript Data Types & Type Coercion — The Complete Guide

5 min read Updated 2026-06-17 Share:

Practice Data Types & Coercion interview questions

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.

More ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel