JavaScript's 7 primitive types plus objects — and the null vs undefined trap every beginner hits.
What it is, in plain English: A data type tells JavaScript what kind of value something is — a number, some text, true/false, and so on. JavaScript has 7 primitive types (string, number, bigint, boolean, undefined, null, symbol) and one complex type (object, which includes arrays and functions). The trickiest part for beginners: null and undefined both mean 'nothing' but are subtly different.
The types of values JavaScript understands
Every piece of information in JavaScript has a type — it tells JavaScript
what kind of thing it's dealing with. You can't add a number to a person's name the same way
you add two numbers together. Types are how JavaScript knows the difference.
The 7 primitive types
A primitive is a simple, single value. JavaScript has seven:
string
"Hello"'world'`template`
Text. Any characters between quotes.
number
423.14-7NaNInfinity
All numbers — integers and decimals share one type.
boolean
truefalse
Yes or no. On or off. Used for decisions.
undefined
undefined
A variable was declared but never given a value.
null
null
Deliberately set to "nothing". A programmer's choice.
bigint
9007199254740993n
Integers too large for a regular number. The n suffix marks it.
symbol
Symbol("id")
A unique, hidden key. Advanced — you won't need this early on.
The complex type: object
Everything that isn't a primitive is an object. Objects hold collections
of values. Arrays, functions, dates, and plain key-value objects are all objects.
// Plain object — key-value pairs
let person = { name: "Alice", age: 30 };
// Array — ordered list of values
let colours = ["red", "green", "blue"];
// Functions are objects too (they can have properties)
function greet() { return "Hello!"; }
console.log(typeof person); // "object"
console.log(typeof colours); // "object"
console.log(typeof greet); // "function" (special sub-type shown by typeof)
Checking the type: typeof
typeof is the operator that tells you what type a value is.
Put it before any expression:
The null / undefined trap — the most common beginner confusion
Both null and undefined mean "nothing" — but they mean it differently:
undefined
null
What it means
"I was declared but nobody gave me a value yet"
"Someone deliberately set me to nothing"
Who sets it
JavaScript (automatically)
You, the programmer
Typical use
Uninitialized variable, missing function argument, property that doesn't exist
Intentionally clearing a value, "no user selected", "no result found"
let x; // declared, never assigned
console.log(x); // undefined — JS set this automatically
let selectedUser = null; // deliberately set to "nothing"
console.log(selectedUser); // null — a programmer's explicit choice
function getUser(id) {
// if no user found, return null — it's an intentional "no result"
if (id === 0) return null;
return { name: "Alice" };
}
let user = getUser(0);
console.log(user); // null
console.log(user === null); // true — checking for null
console.log(user === undefined); // false
Famous JS quirk:typeof null returns "object",
not "null". This is a bug from 1995 that can't be fixed because it would break
the web. To check for null: use value === null, not typeof.
Try It Yourself
Dynamic typing, type checking, and primitives vs references
Dynamic typing
JavaScript is dynamically typed — a variable can hold any type,
and you can change what type it holds at any time. The type belongs to the value,
not the variable.
let x = 42; // x holds a number
console.log(typeof x); // "number"
x = "hello"; // now x holds a string — perfectly valid in JS
console.log(typeof x); // "string"
x = true; // now a boolean
console.log(typeof x); // "boolean"
This flexibility is powerful but dangerous. It means type errors often only appear at
runtime, not when you write the code. TypeScript (a superset of JavaScript) adds static
types to catch these at compile time — a reason it's widely adopted in large codebases.
Primitive values are immutable and compared by value
Primitives can't be changed — you can only replace them. When you compare primitives,
JavaScript compares the actual values:
let a = "hello";
let b = "hello";
console.log(a === b); // true — same value
let n1 = 42;
let n2 = 42;
console.log(n1 === n2); // true — same value
Objects are compared by reference
Objects are stored differently — JavaScript holds a reference (a pointer) to
where the object lives in memory. Two variables can point to the same object, and two
separate objects with identical contents are NOT equal:
let obj1 = { name: "Alice" };
let obj2 = { name: "Alice" };
console.log(obj1 === obj2); // false — different objects in memory
let obj3 = obj1; // obj3 points to the SAME object as obj1
console.log(obj1 === obj3); // true — same reference
obj3.name = "Bob";
console.log(obj1.name); // "Bob" — obj1 changed too! Same object.
Reliable type checking
typeof has limitations (arrays and null both show as "object"). For reliable checks:
// Check for array:
Array.isArray([1, 2, 3]); // true
Array.isArray({}); // false
// Check for null specifically:
let val = null;
val === null; // true (only safe way)
// Check for null OR undefined together:
val == null; // true for both null and undefined
// (one of the few safe uses of ==)
// Object.prototype.toString — the most reliable type check:
Object.prototype.toString.call(42); // "[object Number]"
Object.prototype.toString.call("hi"); // "[object String]"
Object.prototype.toString.call([]); // "[object Array]"
Object.prototype.toString.call(null); // "[object Null]"
Object.prototype.toString.call(undefined); // "[object Undefined]"
Wrapper objects
Primitives can temporarily act like objects. When you call "hello".toUpperCase(),
JavaScript wraps the string primitive in a String object, calls the method,
then discards the wrapper. This is called autoboxing:
// These look like object methods, but the primitive is temporarily boxed:
"hello".length // 5
(42).toString() // "42"
true.toString() // "true"
// Never use new String(), new Number(), new Boolean():
typeof new String("hello") // "object" — confusing, avoid
The type system in the ECMAScript specification
The spec's type taxonomy (ECMA-262 §6)
The spec defines two layers of types: language types (what JS programs work with)
and specification types (internal types the spec uses to describe algorithms,
not visible to programs — e.g. Reference Record, Completion Record, Property Descriptor).
The 8 language types (the 7 primitives + Object) are defined in §6.1. Each has a complete
set of abstract operations: ToPrimitive, ToBoolean, ToNumber,
ToNumeric, ToString, ToObject — these are what JavaScript calls
internally during coercion and operations.
typeof — the spec definition
The typeof operator (§13.5.3) evaluates its operand as a Reference Record without throwing
a ReferenceError even if the variable is undeclared (this is the one case where an undeclared
identifier is safe). It then returns a string based on the type tag. The infamous
typeof null === "object" is the result of the original C implementation storing
type tags in the low bits of a pointer — null's all-zero bit pattern matched the object tag.
ECMA-262 §13.5.3 preserves this as a deliberate historical exception.
Symbol — the identity primitive
Symbols (introduced ES2015) are unique, opaque values. Symbol("desc") creates
a value guaranteed to be unequal to any other value — including another Symbol("desc").
They're used as non-colliding property keys (especially in built-in protocols like
Symbol.iterator, Symbol.toPrimitive, Symbol.toStringTag)
and for "private-ish" properties that don't appear in for...in or Object.keys().
BigInt
BigInt (ES2020) is an arbitrary-precision integer type. Regular numbers (IEEE 754 double)
can only represent integers exactly up to 2⁵³−1 (Number.MAX_SAFE_INTEGER).
BigInt has no upper limit. BigInt and Number are separate types — you can't mix them in
arithmetic (1n + 1 throws TypeError). Use BigInt for: large IDs from databases,
cryptographic integers, precise financial integer arithmetic.
Every object has internal slots (double-bracket notation in the spec: [[Prototype]], [[Extensible]])
and properties stored as Property Descriptors. A data descriptor has
[[Value]], [[Writable]], [[Enumerable]], [[Configurable]]. An accessor descriptor has
[[Get]], [[Set]], [[Enumerable]], [[Configurable]]. Object.defineProperty
exposes this at the language level. Understanding descriptors is the foundation of
metaprogramming, frameworks' reactivity systems, and how Object.freeze works
(sets all [[Writable]] to false, [[Configurable]] to false).