JavaScript · Fundamentals

JavaScript Data Types & Type Coercion — The Complete Guide

5 min read Updated 2026-06-17

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 typesstring, 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.

Practice tests are coming soon

Get notified when interactive mock interviews and quizzes launch.