JavaScript Data Types — Complete Guide with Type Coercion and TypeScript in 2026
Advertisement
Introduction
Why This Matters
JavaScript is a dynamically typed language — variables have no fixed type and the engine performs implicit conversions constantly. This flexibility is a double-edged sword: it makes JavaScript approachable but also produces some of the most baffling bugs in the ecosystem (null == undefined is true, typeof null === 'object', [] + {} === '[object Object]'). Understanding types from first principles is the foundation of writing reliable JavaScript, and it is the prerequisite for working effectively with TypeScript.
Primitive Types (7 Types)
Primitives are immutable values stored directly on the stack. When you assign a primitive to another variable, a full copy is made.
// The 7 primitive types
let str = 'hello' // string
let num = 42 // number
let big = 9007199254740993n // bigint
let bool = true // boolean
let empty = null // null
let undef = undefined // undefined
let sym = Symbol('id') // symbol (unique, opaque identifier)
// Primitives are copied by value
let a = 10
let b = a
b = 99
console.log(a) // still 10 — a and b are independentString
Strings are sequences of UTF-16 code units, zero-indexed and immutable.
const greeting = 'Hello, World!'
console.log(greeting.length) // 13
console.log(greeting[0]) // 'H'
console.log(greeting.toUpperCase()) // 'HELLO, WORLD!'
console.log(greeting.slice(7, 12)) // 'World'
console.log(greeting.includes('World')) // true
// Template literals (ES6+) — preferred for interpolation
const name = 'Alice'
const age = 30
const bio = `Name: ${name}, Age: ${age}` // 'Name: Alice, Age: 30'
// Multi-line strings
const html = `
<div class="card">
<h2>${name}</h2>
</div>
`Number and BigInt
JavaScript has a single number type using 64-bit IEEE 754 floating point. For integers larger than Number.MAX_SAFE_INTEGER (2^53 - 1), use BigInt:
// Number precision limits
console.log(Number.MAX_SAFE_INTEGER) // 9007199254740991
console.log(0.1 + 0.2) // 0.30000000000000004 (float quirk)
console.log(Math.abs(0.1 + 0.2 - 0.3) < Number.EPSILON) // true (correct comparison)
// Special number values
console.log(Infinity) // Infinity
console.log(-Infinity) // -Infinity
console.log(NaN) // NaN
console.log(isNaN('abc')) // true
console.log(Number.isNaN('abc')) // false — strict version (preferred)
console.log(Number.isFinite(42)) // true
// BigInt — for integers beyond safe integer range
const bigNum = 9007199254740993n // trailing 'n'
const doubled = bigNum * 2n // 18014398509481986n
// Cannot mix BigInt and Number without explicit conversion
// bigNum + 1 // TypeError
// BigInt(1) + bigNum // OKBoolean, Null, and Undefined
// Boolean — true/false
const isActive = true
const isPremium = false
// Falsy values — these all coerce to false in boolean contexts
console.log(Boolean(false)) // false
console.log(Boolean(0)) // false
console.log(Boolean(-0)) // false
console.log(Boolean(0n)) // false
console.log(Boolean('')) // false
console.log(Boolean(null)) // false
console.log(Boolean(undefined)) // false
console.log(Boolean(NaN)) // false
// Everything else is truthy, including:
console.log(Boolean([])) // true — empty array is truthy!
console.log(Boolean({})) // true — empty object is truthy!
console.log(Boolean('0')) // true — non-empty string is truthy!
// null vs undefined
let declared // undefined — declared but not assigned
let intentional = null // null — explicitly "no value"
console.log(typeof undefined) // 'undefined'
console.log(typeof null) // 'object' — historical bug in JSSymbol
Symbols are unique, guaranteed-collision-free identifiers, mainly used as object property keys:
const id1 = Symbol('id')
const id2 = Symbol('id')
console.log(id1 === id2) // false — always unique
const USER_ROLE = Symbol('userRole')
const user = { name: 'Alice', [USER_ROLE]: 'admin' }
console.log(user[USER_ROLE]) // 'admin'
console.log(Object.keys(user)) // ['name'] — Symbol keys are hidden
console.log(Object.getOwnPropertySymbols(user)) // [Symbol(userRole)]Reference Types (Objects)
Reference types are stored on the heap. Variables hold a reference (pointer) to the memory location, not the value itself. Assignment copies the reference, not the data.
const obj1 = { x: 1 }
const obj2 = obj1 // copies the reference
obj2.x = 99
console.log(obj1.x) // 99 — both point to the same object!
// To make an independent copy
const obj3 = { ...obj1 } // shallow copy
const obj4 = structuredClone(obj1) // deep copy (ES2022+)Arrays
const langs = ['JavaScript', 'TypeScript', 'Python']
console.log(langs[0]) // 'JavaScript'
console.log(langs.length) // 3
console.log(Array.isArray(langs)) // true — use this, not typeof
// Mutating methods
langs.push('Rust') // add to end
langs.pop() // remove from end
langs.unshift('Go') // add to start
langs.shift() // remove from start
// Non-mutating methods (ES6+)
const upper = langs.map(l => l.toUpperCase())
const js = langs.filter(l => l.includes('Script'))
const found = langs.find(l => l === 'Python')
const has = langs.includes('TypeScript') // trueObjects
const user = {
id: 1,
name: 'Alice',
role: 'admin',
address: {
city: 'San Francisco',
country: 'USA',
}
}
// Access
console.log(user.name) // 'Alice'
console.log(user['role']) // 'admin'
console.log(user.address?.city) // 'San Francisco' (optional chaining)
console.log(user.phone ?? 'N/A') // 'N/A' (nullish coalescing)
// Destructuring
const { name, role, address: { city } } = user
console.log(name, role, city) // 'Alice admin San Francisco'
// Object methods
console.log(Object.keys(user)) // ['id', 'name', 'role', 'address']
console.log(Object.values(user)) // [1, 'Alice', 'admin', {...}]
console.log(Object.entries(user)) // [['id', 1], ['name', 'Alice'], ...]Type Coercion — The Dangerous Part
Implicit type coercion is JavaScript's most error-prone feature. Knowing the rules makes the quirks predictable:
// String concatenation vs numeric addition
console.log(1 + '2') // '12' — 1 coerced to string
console.log('5' - 3) // 2 — '5' coerced to number (- is not overloaded)
console.log(true + true) // 2 — booleans coerce to 1
console.log(null + 1) // 1 — null coerces to 0
console.log(undefined + 1) // NaN — undefined coerces to NaN
// == vs ===
console.log(0 == false) // true — coercion!
console.log(0 === false) // false — no coercion
console.log(null == undefined) // true — special case
console.log(null === undefined) // false
// Always use === unless you explicitly need coercionThe typeof Operator and Its Quirks
console.log(typeof 'hello') // 'string'
console.log(typeof 42) // 'number'
console.log(typeof 42n) // 'bigint'
console.log(typeof true) // 'boolean'
console.log(typeof undefined) // 'undefined'
console.log(typeof Symbol()) // 'symbol'
console.log(typeof function(){}) // 'function'
console.log(typeof {}) // 'object'
console.log(typeof []) // 'object' — not 'array'!
console.log(typeof null) // 'object' — historical bug!
// Better type checks
console.log(Array.isArray([])) // true
console.log(value === null) // null check
console.log(value instanceof Date) // Date check
console.log(Object.prototype.toString.call([])) // '[object Array]'TypeScript Type Annotations
TypeScript adds compile-time type checking that makes JavaScript's dynamic system safe and predictable:
// Primitive type annotations
let name: string = 'Alice'
let age: number = 30
let active: boolean = true
let id: bigint = 1n
let key: symbol = Symbol('key')
let nothing: null = null
let missing: undefined = undefined
// Union types — explicit about multiple possible types
let input: string | number = 'hello'
input = 42 // also valid
// Type narrowing
function process(value: string | number) {
if (typeof value === 'string') {
return value.toUpperCase() // TypeScript knows it's a string here
}
return value * 2 // TypeScript knows it's a number here
}
// Arrays and objects
const nums: number[] = [1, 2, 3]
const pairs: [string, number][] = [['a', 1], ['b', 2]] // tuple array
interface Product {
id: number
name: string
price: number
tags?: string[] // optional
}Common Mistakes
- Using
==instead of===— always use strict equality to avoid unexpected coercion bugs. - Assuming
typeof null === 'null'— it is'object'; always checkvalue === nullexplicitly. - Treating
[]and{}as falsy — they are truthy in boolean contexts. - Confusing
undefined(not yet assigned) withnull(intentionally no value) — they serve different semantic purposes. - Floating point arithmetic without epsilon comparison —
0.1 + 0.2 !== 0.3in JavaScript.
Best Practices
- Always use
===for comparisons unless you have a specific reason to allow coercion. - Use
Number.isNaN()instead of the globalisNaN()for safe NaN checks. - Use
Array.isArray()to check for arrays, nottypeof. - Use
structuredClone()for deep copying objects instead ofJSON.parse(JSON.stringify(...))— it handles more types correctly. - Prefer
constby default; useletonly when reassignment is needed; avoidvar. - Add TypeScript or JSDoc type annotations on function parameters and return types to catch type errors before runtime.
Key Takeaways
- JavaScript has 7 primitive types:
string,number,bigint,boolean,null,undefined, andsymbol. - Primitives are copied by value; reference types (objects, arrays, functions) are copied by reference.
typeof null === 'object'is a known historical bug — always checkvalue === nullexplicitly.typeof []returns'object', not'array'; useArray.isArray()for reliable array detection.- JavaScript has 8 falsy values:
false,0,-0,0n,'',null,undefined, andNaN— everything else is truthy, including[]and{}. - Implicit type coercion with
==and the+operator produces surprising results; prefer===and explicit conversion. Number.MAX_SAFE_INTEGERis 2^53 - 1; useBigIntfor integers larger than this.- TypeScript's type annotations and narrowing eliminate an entire class of runtime type errors at compile time.
Advertisement