skip to content

Runtime Type Detection

There is no single reliable way to ask "what is this value?" in JavaScript. Interviewers want you to know typeof's historical bugs, why instanceof breaks across iframes and realms, and which check to reach for in each situation.

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

questions

4

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

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

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

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