JavaScript Data Types — Complete Guide with Type Coercion and TypeScript in 2026

Sanjeev SharmaSanjeev Sharma
8 min read

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 independent

String

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) &lt; 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  // OK

Boolean, 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 JS

Symbol

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')  // true

Objects

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 coercion

The 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 check value === null explicitly.
  • Treating [] and {} as falsy — they are truthy in boolean contexts.
  • Confusing undefined (not yet assigned) with null (intentionally no value) — they serve different semantic purposes.
  • Floating point arithmetic without epsilon comparison — 0.1 + 0.2 !== 0.3 in JavaScript.

Best Practices

  • Always use === for comparisons unless you have a specific reason to allow coercion.
  • Use Number.isNaN() instead of the global isNaN() for safe NaN checks.
  • Use Array.isArray() to check for arrays, not typeof.
  • Use structuredClone() for deep copying objects instead of JSON.parse(JSON.stringify(...)) — it handles more types correctly.
  • Prefer const by default; use let only when reassignment is needed; avoid var.
  • 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, and symbol.
  • Primitives are copied by value; reference types (objects, arrays, functions) are copied by reference.
  • typeof null === 'object' is a known historical bug — always check value === null explicitly.
  • typeof [] returns 'object', not 'array'; use Array.isArray() for reliable array detection.
  • JavaScript has 8 falsy values: false, 0, -0, 0n, '', null, undefined, and NaN — everything else is truthy, including [] and {}.
  • Implicit type coercion with == and the + operator produces surprising results; prefer === and explicit conversion.
  • Number.MAX_SAFE_INTEGER is 2^53 - 1; use BigInt for integers larger than this.
  • TypeScript's type annotations and narrowing eliminate an entire class of runtime type errors at compile time.

Advertisement

Sanjeev Sharma

Written by

Sanjeev Sharma

Full Stack Engineer · E-mopro

Related reading