In JavaScript, "variables don't have types — values do". A variable can hold a string today and a number tomorrow, and that's perfectly legal. That's why understanding the type system matters more here than in strict languages: it isn't checked by a compiler — it operates right at runtime, and it surprises those who don't know it.
Seven primitives and everything else
The primitive types: string, number, boolean, undefined, null, symbol, bigint. Everything else is an object: plain objects, arrays, functions, dates, regular expressions.
typeof "hi"; // "string"
typeof 42; // "number"
typeof true; // "boolean"
typeof undefined; // "undefined"
typeof Symbol(); // "symbol"
typeof 10n; // "bigint"
typeof {}; // "object"
typeof []; // "object" — an array is an object too!
typeof function(){} // "function" — a special answer for functions
typeof null; // "object" — the language's famous bug
Two of typeof's answers deserve a comment. typeof null === "object" is a historical mistake from the first implementation that can't be fixed without breaking the internet: check for null with the comparison x === null. And "function" is a courtesy: functions are formally objects, but typeof singles them out.
typeof can't tell an array from an object — that's what Array.isArray() is for:
Array.isArray([]); // true
Array.isArray({}); // false
undefined and null: two kinds of "nothing"
The language has two "empty" values, and by convention they mean different things:
undefined— "there was no value yet": an undeclared property, a parameter with no argument, a function withoutreturn.null— "the value is intentionally empty": it is assigned explicitly to denote a deliberate absence.
let user;
user; // undefined — never assigned
const found = null; // null — we searched and deliberately found nothing
The practical consequence: if you see undefined in your data, most likely someone forgot to assign or mistyped a property name; null means someone put the emptiness there on purpose. Checking "neither one nor the other" in a single line: x == null — a rare case where loose comparison is deliberately useful.
number: one type for everything, plus NaN
JavaScript has no separate integers and floats — only number (a 64-bit float). Hence two classics:
0.1 + 0.2 === 0.3; // false! 0.30000000000000004
Number.MAX_SAFE_INTEGER; // 9007199254740991 — beyond this, integers "lie"
Fractional money is never computed in number "head-on" — it's computed in cents or via string-based libraries; for giant integers there's bigint.
NaN ("not a number") is the result of a meaningless numeric operation. Its treachery: NaN is the only value not equal to itself:
NaN === NaN; // false
Number.isNaN(NaN); // true — the correct check
If the comparison x === x yields false, you're looking at NaN. That's not a joke — it's a real technique from old code.
Value versus reference
The main behavioral fork: primitives are copied by value, objects — by reference.
let a = 1;
let b = a;
b = 2;
a; // 1 — the copy is independent
const x = { n: 1 };
const y = x;
y.n = 2;
x.n; // 2! — x and y point to the SAME object
This is the foundation on which both copying objects and React's state-update rules stand. For now, keep the rule itself in mind: assigning an object doesn't create a new object — only a second reference to the same one.
Boxing: why a string has methods
A string is a primitive, yet "hi".toUpperCase() works. How? When you access a property of a primitive, the engine momentarily wraps it in an object (String, Number, Boolean), calls the method, and throws the wrapper away. Hence the rule: the wrappers exist, but you should never create them by hand (new String("hi")) — you'll get an object with all the surprises of object behavior:
typeof new String("hi"); // "object", not "string"
new Boolean(false) ? 1 : 2; // 1! an object is always truthy
In short
- Types belong to values, not variables. Seven primitives; everything else is an object (arrays and functions too).
typeof null === "object"is a language bug; check null with=== null, arrays withArray.isArray().undefined— "never assigned",null— "empty on purpose".x == nullcatches both.- One numeric type:
0.1 + 0.2 !== 0.3, money goes in cents.NaNisn't equal to itself; the check isNumber.isNaN(). - Primitives are copied by value, objects by reference. That's the cause of half the "mystical" bugs.
- Primitive wrappers work automatically;
new String()by hand — never.
What to read next
- Type coercion — what happens when types meet in one expression.
- Objects: references, copying, and immutability — the continuation of the "by value vs by reference" theme.
- JavaScript essentials for TypeScript — how static typing sits on top of this type system.