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, and NaN (“Not a Number”).
  • bigint — integers of arbitrary size, written with an n suffix like 123n, for when numbers exceed Number.MAX_SAFE_INTEGER.
  • booleantrue or false.
  • 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—typeof reports arrays as "object", which is not specific enough.
  • Prefer value === null or value == null (deliberately) over typeof value === "object" when checking for null, 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() and Number.isFinite() instead of the global isNaN()/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], or structuredClone(obj) when you need an independent copy.
  • Use bigint only when you truly need integers beyond Number.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 plain typeof cannot 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.log statements 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) plus object.
  • 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 null returns "object"—a long-standing language quirk, not a sign that null is an object.
  • Use Array.isArray() for arrays and strict equality (===) everywhere to avoid coercion surprises.
  • NaN is never equal to itself; detect it with Number.isNaN().
  • Explicit conversion (Number(), String(), Boolean()) makes code more predictable than relying on implicit coercion.