03 — Data Types — Primitives vs Objects, and the typeof Quirks

5 min read

"Numbers, strings, and objects" was my entire type model, and it explained none of the surprises. The idea that everything else hangs off: there are exactly two categories of value — primitives (copied by value) and objects (copied by reference) — and the seven primitive types are the only things on the primitive side. [1]

The framing that finally landed is the split, not the list. Most of JavaScript's type-related surprises — mutation bugs, typeof quirks, equality weirdness — collapse to one question: is this value a primitive or an object? Primitives are immutable and assigned/copied by value. Objects are mutable and assigned/copied by reference. Everything follows from there.

Primitives — by value Number String Boolean Null Undefined Symbol BigInt immutable · copied by value Objects — by reference Object Array Function Date, RegExp Map, Set Error mutable · copied by reference

The seven primitives

Each primitive is a single, immutable value [1][2]:

  • Number — one numeric type for both integers and floats, stored as IEEE 754 double-precision. Special values: Infinity, -Infinity, NaN. The "all numbers are floats" design is why 0.1 + 0.2 !== 0.3 [3].
  • BigInt — for integers beyond Number.MAX_SAFE_INTEGER (2⁵³−1). Written with an n suffix: 9007199254740993n. Needed for crypto, large counters, exact integer math [4].
  • String — a sequence of characters. Single quotes, double quotes, or backticks (template literals). Immutable — "editing" a string always creates a new one.
  • Boolean — true or false. The trap is truthy and falsy: most values coerce to true, but 0, "", null, undefined, NaN, and false itself are falsy.
  • null — intentional absence of value. The typeof quirk: typeof null === "object", a bug from the original implementation that can't be fixed without breaking the web [5].
  • undefined — a variable declared but not assigned, or a function that returned nothing. The default "not set" sentinel.
  • Symbol — a unique, immutable identifier, mainly used as hidden object property keys to avoid collisions [6].

The difference between null and undefined is one I used to blur: null means I deliberately put nothing here; undefined means nothing was ever set here. Checking === null versus === undefined is how I tell intent from absence.

Objects: the reference side

Anything not in the primitive list is an object — plain objects, arrays, functions, dates, regexps, maps, sets, errors. The defining trait isn't the shape; it's that they're passed by reference [7]:

const a = { count: 1 };
const b = a; // b points to the SAME object
b.count = 2;
console.log(a.count); // 2 — a sees the change

Two names, one object. That's why mutating a passed-in argument inside a function changes the caller's data — a classic source of bugs. Primitives don't do this; assigning let x = 5; let y = x; copies the value, and changing y leaves x alone.

The typeof operator

typeof returns a string describing a value's type [8]. It's the quick type check, and it has two famous quirks worth memorizing:

Expression

Returns

Notes

typeof 5

"number"

typeof "hi"

"string"

typeof true

"boolean"

typeof undefined

"undefined"

typeof Symbol()

"symbol"

typeof 5n

"bigint"

typeof null

"object"

the famous bug

typeof function(){}

"function"

functions are a subtype

typeof {}

"object"

arrays, dates, regexps too

The typeof null === "object" trap is why null checks use === null, never typeof. And typeof can't distinguish arrays from plain objects — for that, Array.isArray() is the right tool.

How I use this

Two habits fall straight out of the primitive/object split. First, when a value crosses a function boundary and I don't want surprises, I copy it ({ ...obj } for shallow, structuredClone for deep) rather than trusting the caller not to mutate. Second, type checks: typeof for primitives, Array.isArray() for arrays, instanceof for class instances, and === null / === undefined for those two specifically — never typeof. The NaN edge case I now handle with Number.isNaN(), which is correct, unlike the global isNaN() that coerces.

References

[1] Mozilla, "JavaScript data types and data structures," MDN Web Docs, 2024. [Online]. Available: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Data_structures

[2] I. Kantor, "Data types," The Modern JavaScript Tutorial, 2024. [Online]. Available: https://javascript.info/types

[3] Mozilla, "Number — IEEE 754 double-precision," MDN Web Docs, 2024. [Online]. Available: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number

[4] Mozilla, "BigInt," MDN Web Docs, 2024. [Online]. Available: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt

[5] Altcademy, "What is null in JavaScript," 2023. [Online]. Available: https://www.altcademy.com/blog/what-is-null-in-javascript/

[6] JavaScript Tutorial, "Symbol data type in JavaScript," 2023. [Online]. Available: https://www.javascripttutorial.net/symbol/

[7] I. Kantor, "Objects," The Modern JavaScript Tutorial, 2024. [Online]. Available: https://javascript.info/object

[8] Mozilla, "typeof operator," MDN Web Docs, 2024. [Online]. Available: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/typeof

Knowledge check · Question 1 of 5

`typeof null` returns…

Comments

Leave a Comment

You must be signed in to comment

0 Comments

No comments yet. Be the first to comment!