JavaScript Data Types
Every value in JavaScript has a type, even though JavaScript is a dynamically typed language, meaning you never declare the type of a variable yourself—the type is attached to the value, and a variable can hold any type at any time. Understanding the type system is essential because it explains why "5" + 3 gives you "53" while "5" - 3 gives you 2, why comparing objects can surprise you, and why some values are copied while others are shared by reference. This lesson covers every JavaScript type in depth, how the engine stores and compares them, and the mistakes that trip up even experienced developers.
Overview / How it works
JavaScript has exactly eight data types. Seven of them are called primitives: string, number, bigint, boolean, undefined, null, and symbol. The eighth is object, which is the umbrella type for everything else—plain objects, arrays, functions, dates, maps, sets, regular expressions, and more.
The distinction between primitives and objects is not just academic—it determines how values behave in memory. Primitive values are immutable and are typically stored directly wherever the variable lives (conceptually on the “stack” for simple engines, though real engines like V8 use more sophisticated strategies). When you assign a primitive to another variable, the value is copied. Objects, on the other hand, live in a separate memory area (the “heap”), and a variable holding an object doesn’t store the object itself—it stores a reference (essentially a pointer) to where that object lives. When you assign an object to another variable, you copy the reference, not the object, so both variables end up pointing at the same underlying data.
This is why mutating an object through one variable is visible through any other variable that references the same object, while reassigning a primitive through one variable never affects a copy held elsewhere. Keeping this mental model—primitives copy by value, objects copy by reference—in mind resolves the majority of confusion new developers have about JavaScript’s behavior.
The primitive types in brief
- string — textual data, e.g.
"hello", always immutable (string methods return new strings, they never mutate). - number — all numbers, integer or floating point, using the IEEE-754 double-precision format. Includes special values
Infinity,-Infinity, andNaN(“Not a Number”). - bigint — integers of arbitrary size, written with an
nsuffix like123n, for when numbers exceedNumber.MAX_SAFE_INTEGER. - boolean —
trueorfalse. - undefined — the automatic value of a declared-but-unassigned variable, a missing function argument, or a missing object property.
- null — an intentional, explicit “no value,” assigned by you, never by the engine automatically.
- symbol — a guaranteed-unique value, often used as a “hidden” object property key that won’t collide with string keys.
Syntax
You don’t declare types in JavaScript—the type is inferred from the literal or value you assign. The general form is simply a variable declaration with a value:
const name = "value"; // string
const count = 42; // number
const big = 42n; // bigint
const flag = true; // boolean
let nothingYet; // undefined
const empty = null; // null
const id = Symbol("id"); // symbol
const user = { name: "Ada" }; // object
const list = [1, 2, 3]; // object (array)
| Operator / Function | Purpose |
|---|---|
typeof value |
Returns a string naming the primitive type (with a famous exception for null) |
Array.isArray(value) |
Correctly checks whether a value is an array (typeof arrays is just “object”) |
value === null |
The reliable way to check specifically for null |
Number(value), String(value), Boolean(value) |
Explicit conversion between types |
Number.isNaN(value) |
Reliably checks for the special NaN number value |
Examples
Example 1: Inspecting every type with typeof
const age = 30;
const name = "Ada";
const isActive = true;
const id = Symbol("id");
const big = 9007199254740993n;
let notAssigned;
const empty = null;
console.log(typeof age);
console.log(typeof name);
console.log(typeof isActive);
console.log(typeof id);
console.log(typeof big);
console.log(typeof notAssigned);
console.log(typeof empty);
console.log(typeof { a: 1 });
console.log(typeof [1, 2, 3]);
console.log(typeof function () {});
Output:
number
string
boolean
symbol
bigint
undefined
object
object
object
function
Notice two quirks: typeof null returns "object", which is a decades-old bug baked into the language for backward compatibility—null is not actually an object, it’s its own primitive type. And typeof for a function returns "function" even though functions are technically a specialized kind of object under the hood.
Example 2: Primitives copy by value, objects copy by reference
let a = 10;
let b = a;
b = 20;
console.log(a, b);
const obj1 = { score: 10 };
const obj2 = obj1;
obj2.score = 20;
console.log(obj1.score, obj2.score);
Output:
10 20
20 20
When b is assigned a‘s value, it receives an independent copy of the number 10, so changing b later has no effect on a. But when obj2 is assigned obj1, both variables point to the exact same object in memory. Mutating a property through obj2 is visible through obj1 too, because there was never a second object—only a second reference to the same one.
Example 3: Type coercion in real code
function calculateTotal(priceInput, quantityInput) {
const price = Number(priceInput);
const quantity = Number(quantityInput);
return price * quantity;
}
console.log(calculateTotal("19.99", "3"));
console.log("5" + 3);
console.log("5" - 3);
console.log(1 + true);
console.log([1, 2] + [3, 4]);
Output:
59.97
53
2
2
1,23,4
Form inputs, URL parameters, and JSON values often arrive as strings even when they represent numbers, so explicit conversion with Number(...) (as in calculateTotal) is a common, deliberate pattern. But implicit coercion, driven by JavaScript’s operators, is more surprising: + prefers string concatenation whenever either operand is a string, so "5" + 3 becomes "53". Meanwhile - has no string meaning, so it always coerces both sides to numbers, giving "5" - 3 === 2. Booleans coerce to 0/1 in arithmetic, so 1 + true is 2. And arrays are objects, so + first converts each array to a string (joining elements with commas) before concatenating, producing "1,23,4".
Under the hood
When the JavaScript engine (V8, SpiderMonkey, JavaScriptCore) evaluates const x = 10;, it stores the number directly in the variable’s memory slot. For const user = { name: "Ada" };, the engine allocates a block of memory on the heap to hold the object’s properties, and the variable user stores only the address of that block. Copying a variable (const user2 = user;) copies whatever is in the slot—a raw value for primitives, an address for objects—which is exactly why the two categories behave so differently on assignment, on function calls (arguments are passed the same way variables are copied), and on equality checks (=== compares object references, not their contents, so two separately created objects with identical properties are never === to each other).
Strings deserve a special note: although they can be indexed like arrays ("hello"[0]) and have a .length, they are primitives, not objects. When you call a method like "hello".toUpperCase(), the engine briefly wraps the primitive in a temporary String wrapper object, calls the method, and discards the wrapper—you never notice this happening, but it explains why you can call methods on primitives at all despite them not being objects.
Common Mistakes
Mistake 1: Relying on loose equality (==) and getting unexpected coercion.
console.log(0 == "0");
console.log(0 == "");
console.log(0 == false);
console.log("" == false);
console.log(null == undefined);
console.log(null === undefined);
Output:
true
true
true
true
true
false
== converts operands to a common type before comparing, which produces a long list of surprising “true” results for values that are conceptually quite different. The fix is simple: use === and !== almost everywhere, which compare both value and type with no coercion. Reserve == null only for the rare, deliberate idiom of checking for both null and undefined at once.
Mistake 2: Comparing against NaN with ===.
const result = Number("abc");
console.log(result);
console.log(result === NaN);
console.log(Number.isNaN(result));
console.log(Object.is(result, NaN));
Output:
NaN
false
true
true
NaN is the only value in JavaScript that is never equal to itself, so result === NaN always returns false no matter what result is. Use Number.isNaN(value) (not the older, less strict global isNaN(value), which coerces its argument first) to reliably detect it.
Best Practices
- Use
===and!==instead of==and!=to avoid unpredictable type coercion. - Use
Array.isArray(value)to check for arrays—typeofreports arrays as"object", which is not specific enough. - Prefer
value === nullorvalue == null(deliberately) overtypeof value === "object"when checking fornull, since the latter is misleading. - Convert types explicitly (
Number(x),String(x),Boolean(x)) instead of relying on implicit coercion in arithmetic or concatenation—explicit code is easier to read and debug. - Use
Number.isNaN()andNumber.isFinite()instead of the globalisNaN()/isFinite(), which coerce their argument before checking and can hide bugs. - Remember that objects (including arrays and functions) are always passed and assigned by reference—clone with
{ ...obj },[...arr], orstructuredClone(obj)when you need an independent copy. - Use
bigintonly when you truly need integers beyondNumber.MAX_SAFE_INTEGER(2^53 – 1); it cannot be mixed with regular numbers in arithmetic without explicit conversion.
Practice Exercises
- Exercise 1: Write a function
describeType(value)that returns a string describing the value’s type, correctly distinguishing"array","null", and"object"from one another (remember that plaintypeofcannot do this alone). - Exercise 2: Predict the output of
console.log(typeof typeof 1)before running it, and explain in your own words why the result is what it is. - Exercise 3: Create an object representing a shopping cart item, assign it to a second variable, and demonstrate with
console.logstatements how mutating the second variable’s property affects the first—then show how to avoid that by creating a shallow copy instead.
Summary
- JavaScript has eight types: seven primitives (
string,number,bigint,boolean,undefined,null,symbol) plusobject. - Primitives are immutable and copied by value; objects are copied by reference, so multiple variables can point at (and mutate) the same underlying data.
typeof nullreturns"object"—a long-standing language quirk, not a sign thatnullis an object.- Use
Array.isArray()for arrays and strict equality (===) everywhere to avoid coercion surprises. NaNis never equal to itself; detect it withNumber.isNaN().- Explicit conversion (
Number(),String(),Boolean()) makes code more predictable than relying on implicit coercion.
