skip to content

Primitives and Objects

Every JavaScript value is either one of the seven primitives or an object, and that single split drives copy semantics, mutability, and how you can inspect a value at runtime. Most coercion surprises trace back to a primitive being boxed or an object being flattened into one.

part ofJavaScriptoverview, primer and where to startread it →
on this pageshow

explore

questions

13

In JavaScript, what does the typeof operator return for null, for an array, for a function or class, and for a variable that was never declared?

level: juniorimportance: must knowfreq 82%

answer

  1. one operator, a fixed set of strings
  2. arrays and dates look identical
  3. callables get their own result
  4. safe on a name that never existed
  5. null shares the object tag

basics

~20 s

typeof null returns "object" — a historic bug kept for compatibility. Arrays return "object" too, so typeof cannot spot them. Functions and classes return "function". typeof on a never-declared name safely returns "undefined" instead of throwing.

solid answer

~50 s

`typeof` returns a string naming a value's type, and the corners matter more than the happy path. `typeof null` is `"object"`, a bug inherited from the very first implementation where the null pointer carried the same internal tag as objects; it was never fixed because too much code depends on it, so a null test needs `value === null`, or `value !== null && typeof value === "object"` for "a real object". Arrays, dates, regexes and plain objects all report `"object"`, so `typeof` cannot tell them apart. Anything callable — function declarations, arrow functions, and `class` declarations — reports `"function"`. And `typeof` is the only operator that tolerates a completely undeclared name: `typeof missing` evaluates to `"undefined"` rather than throwing a ReferenceError, which is why old feature-detection code was written that way. The one exception is a `let` or `const` binding used before its declaration, which still throws.

code

javascript · 10 lines
javascript
console.log(typeof neverDeclaredAnywhere); // "undefined" — no ReferenceError

function isObject(value) {
  return value !== null && typeof value === "object";
}

console.log(isObject(null));       // false
console.log(isObject([]));         // true
console.log(isObject(new Date())); // true
console.log(typeof class Foo {});  // "function"

go deeper

for a junior

Memorise the odd ones: null gives "object", arrays give "object", functions and classes give "function". Say plainly that a null test needs === null, and that typeof cb === "function" is the right guard before calling a callback.

for a middle

Explain the mechanism, not just the table: the historic type-tag collision behind null, the internal call method behind "function", and the specification's special case that lets typeof survive an unresolvable identifier while a temporal-dead-zone binding still throws.

for a senior

Show where a typeof-based guard silently lets bad input through in production — an array or a date sliding past an === "object" check — and describe what you reach for instead when a value crosses a module or process boundary.

for a principal

Frame the tradeoff: typeof is cheap, total and forgery-proof but almost information-free. Be ready to argue when a codebase should stop hand-rolling runtime type guards at all and push the check to a validated boundary instead of scattering them.

## What typeof actually is `typeof` is a unary operator, not a function, that evaluates its operand and returns one of a small fixed set of strings. Since BigInt landed in ES2020 the set is: `"undefined"`, `"boolean"`, `"number"`, `"bigint"`, `"string"`, `"symbol"`, `"object"`, and `"function"`. The rule is mechanical: every primitive gets its own string, every object gets `"object"` unless it is callable, in which case it gets `"function"`. There is no `"array"`, no `"date"`, no `"null"`, and no `"class"`. ```js typeof undefined // "undefined" typeof true // "boolean" typeof 42 // "number" typeof 10n // "bigint" typeof "hi" // "string" typeof Symbol() // "symbol" typeof {} // "object" typeof [] // "object" typeof null // "object" typeof function () {} // "function" typeof class {} // "function" ``` ## Why typeof null is "object" In the original implementation, values were stored as a small type tag plus a payload, and the tag `0` meant "object". The null value was represented as the machine null pointer, so its tag read as `0` and `typeof` reported `"object"`. It is a bug, not a design statement — null is a primitive, and the language has a separate Null type. A proposal to correct it to `"null"` was rejected because an enormous amount of deployed code branches on `typeof x === "object"` and then handles null inside that branch; changing the result would silently reroute all of it. The practical consequence is that `typeof` alone is never a sufficient object test: ```js function isObject(value) { return value !== null && typeof value === "object"; } ``` That predicate is still deliberately broad — it is true for arrays, dates, regexes and class instances, because all of those really are objects. ## Callables `"function"` is the one case where `typeof` looks past the value's type at an internal capability: any object with a `[[Call]]` internal method reports `"function"`. That covers function declarations, function expressions, arrow functions, methods, generator functions, async functions and `class` declarations. A class is a function object at runtime, so `typeof MyClass === "function"` — a common trip-up for people arriving from languages where a class is its own kind of entity. Correspondingly, `typeof callback === "function"` is the idiomatic and correct guard before invoking something a caller handed you. It is one of the few checks `typeof` is genuinely good at. ## Undeclared names, and the exception Reading an identifier that was never declared anywhere throws a ReferenceError. `typeof` is special-cased in the specification: if the operand is a plain identifier reference that cannot be resolved, the operator returns `"undefined"` instead of throwing. This is why pre-module feature detection looked like `if (typeof SomeGlobal !== "undefined")`. ```js console.log(typeof neverDeclaredAnywhere); // "undefined", no throw console.log(neverDeclaredAnywhere); // ReferenceError ``` The exception is a `let` or `const` binding touched before its declaration is evaluated. That binding *does* exist in the scope; it is merely uninitialised, so the reference resolves and then fails on the uninitialised check. `typeof` gets no free pass there and throws a ReferenceError. Note also that `typeof` says nothing about whether a *property* exists: `typeof obj.missing` is `"undefined"` for a property that is absent and for one explicitly set to `undefined`. Distinguishing those needs the `in` operator or `Object.hasOwn`. ## What typeof cannot do Because every non-callable object collapses to `"object"`, `typeof` cannot answer any of the questions people actually want answered: is this an array, a date, a Map, an instance of my class, a plain object? Each of those needs a different tool — a dedicated predicate, a brand read, or a prototype-chain test. Treat `typeof` as a primitive-vs-object-vs-callable filter, not as a type system. One last legacy oddity worth recognising: the specification's Annex B allows a host-provided legacy object whose `typeof` is `"undefined"` even though the value is an object (browsers use this for `document.all`). It exists purely to keep 1990s browser-sniffing code working, and you should never rely on it.

  • Why was typeof null never corrected to return "null"?
    It was proposed and rejected. A vast amount of deployed code tests `typeof x === "object"` and handles the null case inside that branch. Returning `"null"` would silently push null values down a different path in code nobody is going to revisit, so the committee kept the bug. The workaround is trivial — compare with `=== null` — which further weakened the case for breaking the web.
  • typeof missingGlobal returns "undefined" instead of throwing, so is typeof ever able to throw a ReferenceError?
    Yes. The free pass only covers an identifier that resolves to nothing at all. A `let` or `const` binding used before its declaration line does exist in the scope but is uninitialised, so `typeof x` on it throws a ReferenceError. The same applies to a class binding. Only genuinely undeclared names are safe.
  • How would you distinguish a property that is absent from one that is present but set to undefined?
    `typeof` cannot: both give `"undefined"`. Use `"key" in obj`, which also sees inherited properties, or `Object.hasOwn(obj, "key")` (ES2022) for own properties only. `Object.keys` and destructuring likewise treat a present-but-undefined property as present, which is why default parameter values fire for it.

saying these in an interview costs you the question

  • Says typeof null returns the string "null"
  • Uses typeof to tell arrays apart from plain objects
  • Thinks typeof NaN is "NaN" or "undefined"
  • Claims typeof on any undeclared name always throws
  • Believes typeof of a class is "object" or "class"

context

open as a page

Name the seven primitive types in JavaScript and explain what makes a value a primitive rather than an object.

level: juniorimportance: must knowfreq 80%

basics

~10 s

JavaScript has seven primitives: string, number, boolean, null, undefined, symbol and bigint. Everything else is an object. Primitives are immutable, hold no properties of their own, and are copied and compared by their value.

open as a page

An array built inside an iframe is handed to the parent page, where value instanceof Array evaluates to false even though the value genuinely is an array. Why does instanceof fail here, and what check works instead?

level: middleimportance: must knowfreq 58%

basics

~20 s

Each iframe is a separate realm with its own Array constructor and Array.prototype, so a foreign array's prototype chain never reaches the parent's Array.prototype and instanceof is false. Array.isArray reads the internal array brand and works across realms.

open as a page

In JavaScript, how does assigning a primitive to another variable differ from assigning an object, and what does that mean for a value you pass into a function?

level: middleimportance: must knowfreq 78%

basics

~20 s

Assignment always copies a value. For a primitive that value is the data itself, so the two variables are independent. For an object the value is a reference, so both names point at one object and a mutation through either is visible through both.

open as a page

In JavaScript, what is the difference between null and undefined, and why does a default parameter value apply when an argument is undefined but not when it is null?

level: middleimportance: must knowfreq 70%

basics

~20 s

undefined is the absence the language itself produces: unassigned variables, missing arguments, missing properties, functions with no return. null is an absence a programmer assigns deliberately. Default parameter values fire only for undefined, never for null.

open as a page

String primitives have no properties of their own, so why does `'abc'.length` work, and why does assigning `s.foo = 1` to a string variable never stick?

level: middleimportance: must knowfreq 70%

basics

~20 s

Property access on a primitive makes the engine wrap it in a temporary String, Number, Boolean, Symbol or BigInt object, read the property from that wrapper, then discard it. Writes land on the throwaway wrapper, so they vanish.

open as a page

In JavaScript, what does `new String('a')` produce, and how does it differ from the primitive string `'a'`?

level: juniorimportance: should knowfreq 55%

basics

~10 s

new String('a') creates an object wrapper, not a string: typeof reports 'object' and strict equality with 'a' is false. Calling String('a') without new returns the primitive, which is what you almost always want.

open as a page

Why do developers write Object.prototype.toString.call(value) instead of value.toString() to identify a value in JavaScript, and what does it return for null, [] and new Date()?

level: middleimportance: should knowfreq 40%

basics

~10 s

Object.prototype.toString.call(value) returns a brand string — "[object Null]", "[object Array]", "[object Date]" — read from the value's internal representation. Calling value.toString() instead runs each type's overridden version, and throws outright on null and undefined.

open as a page

Why does `if (new Boolean(false)) { ... }` run its block, and what bug does that cause in code that builds flags with `new Boolean(x)`?

level: middleimportance: should knowfreq 45%

basics

~10 s

new Boolean(false) is an object, not the value false, and every object is truthy — so the condition passes. The wrapped false is only visible through valueOf, which a plain if-test never consults.

open as a page

A JavaScript helper is written as `function request(url, opts = DEFAULTS) { opts.retries -= 1; /* ... */ }`, where `DEFAULTS` is a module-level object literal. Each test passes when run alone, but the suite fails when the tests run together. What is happening, and how would you fix it?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Every call that omits opts receives the same DEFAULTS object, not a copy, so opts.retries -= 1 mutates shared module state that accumulates across calls. Order-dependent failures follow. Fix it by never mutating a parameter and building a fresh per-call object instead.

open as a page

Why does minified JavaScript often contain `void 0` where the source said `undefined`, and what does the `void` operator actually do?

level: middleimportance: nice to knowfreq 25%

basics

~20 s

The void operator evaluates its operand, discards the result, and always produces the value undefined. Minifiers emit void 0 because it is shorter than the nine-character identifier undefined and cannot be affected by a local binding that shadows that identifier.

open as a page

A bundle ends up containing two copies of your library, so a Money object made by one copy fails value instanceof Money inside the other. How would you write a type check for Money that still recognises it — across duplicate copies and across realms?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

Stop keying the check on class identity. Tag instances with a property whose key is a registry symbol such as Symbol.for("acme.money"), since that key is identical in every copy and every realm, and test for it — optionally exposing it through static [Symbol.hasInstance] so instanceof keeps working.

open as a page

A shared utility keeps receiving values that print as ordinary strings but fail `typeof x === 'string'` and compare unequal to identical literals. How do you confirm they are boxed String objects, where do such values come from, and how do you harden the code?

level: seniorimportance: nice to knowfreq 25%

basics

~20 s

Check typeof and instanceof: a boxed value reports 'object' and is an instanceof String while logging identically to the primitive. They usually come from Object(), a generic map(Object), or a sloppy-mode this. Unwrap at the boundary with String(x) and fix the producer.

open as a page