InterviewsVector
beginnerRare5 min read · Updated Jul 18, 2026

Build a Reliable typeof: Type Checking with Object.prototype.toString

Why typeof lies (null, arrays, NaN), how Object.prototype.toString.call gives reliable type tags, and how to build the getType utility interviewers ask for.


The problem

typeof says null is an "object", arrays are "object"s, and NaN is a "number". Build a getType(value) that returns a precise, lowercase type name for any value: "null", "array", "date", "map", "regexp", and so on.

This question checks whether you know why typeof's quirks exist, and whether you know the one reliable primitive underneath every type-checking utility in lodash, Node's util, and countless codebases: Object.prototype.toString.call.

Why typeof null === "object"

In the original 1995 implementation, values were tagged words: the low bits encoded the type, and the tag for objects was 000. null was the NULL pointer — all zero bits — so it matched the object tag. The bug shipped, the web depended on it, and a 2007 proposal to fix it was rejected for compatibility. It is a bug preserved as a spec guarantee — exactly the kind of "why" answer interviewers want instead of a memorized fact.

The other classic gaps:

typeof null;          // "object"  — the historical bug
typeof [];            // "object"  — arrays are objects; use Array.isArray
typeof NaN;           // "number"  — NaN is a numeric value (IEEE 754)
typeof function(){};  // "function" — the only object with its own tag
typeof document.all;  // "undefined" — the weirdest carve-out in the language

The wrong way: instanceof and .toString()

A first draft usually chains instanceof Date, instanceof RegExp, or calls value.toString(). Three failure modes worth naming:

  1. instanceof fails across realms — an array from an iframe or a worker message is not instanceof your window's Array. This is precisely why Array.isArray exists.
  2. value.toString() is user-overridable — any object can lie about itself, and Object.create(null) has no toString at all, so the check throws.
  3. A ladder of instanceof checks doesn't scale — every new type is another branch.

The right primitive

Every object carries an internal type tag readable through the original toString on Object.prototype, called with the value as this:

Object.prototype.toString.call([]);            // "[object Array]"
Object.prototype.toString.call(null);          // "[object Null]"
Object.prototype.toString.call(new Date());    // "[object Date]"
Object.prototype.toString.call(/x/);           // "[object RegExp]"
Object.prototype.toString.call(new Map());     // "[object Map]"
Object.prototype.toString.call(Promise.resolve()); // "[object Promise]"
Object.prototype.toString.call(Object.create(null)); // "[object Object]" — no throw

(The .call here is method borrowing — the same move you implement in custom call.)

Implementation

function getType(value) {
  // Extract "Array" from "[object Array]" and lowercase it.
  return Object.prototype.toString.call(value)
    .slice(8, -1)
    .toLowerCase();
}

That one-liner already beats the instanceof ladder:

getType(null);            // "null"
getType(undefined);       // "undefined"
getType([1, 2]);          // "array"
getType(new Date());      // "date"
getType(/ab+c/);          // "regexp"
getType(new Map());       // "map"
getType(new WeakSet());   // "weakset"
getType(Symbol("s"));     // "symbol"
getType(10n);             // "bigint"
getType(() => {});        // "function"
getType(async () => {});  // "asyncfunction"
getType(function* () {}); // "generatorfunction"
getType(new Error("x"));  // "error"
getType({});              // "object"

If the interviewer wants NaN distinguished, add one branch — and use Number.isNaN, not the coercing global isNaN:

function getType(value) {
  if (typeof value === "number" && Number.isNaN(value)) return "nan";
  return Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
}

Edge cases interviewers probe

  • Cross-realm values — the tag-based approach works where instanceof fails; this is its selling point.
  • Object.create(null) — no inherited methods; any solution that calls value.toString() crashes. Borrowed Object.prototype.toString.call doesn't.
  • Boxed primitivesgetType(new Number(5))"number", same tag as the primitive. If the distinction matters, check typeof value === "object" first. (Also worth saying: boxed primitives in real code are a smell.)
  • Symbol.toStringTag can lie({ [Symbol.toStringTag]: "Array" }) reports "[object Array]". Since ES6 the tag is spoofable for custom objects (built-ins like the actual array check in Array.isArray are not fooled). So: Array.isArray for security-relevant array checks, tag-based getType for diagnostics.
  • typeof is safe on undeclared variablestypeof notDeclared returns "undefined" where a bare reference throws a ReferenceError. It's the only operator with this property, which is why feature detection uses it.

Test yourself

Predict the outputWhat does this code print?
console.log(
typeof null,
typeof NaN,
typeof [],
typeof (() => {})
);

Common mistakes

  • Building the instanceof ladder and never mentioning realms.
  • isNaN instead of Number.isNaN (isNaN("foo") is true — coercion strikes again).
  • Calling value.toString() instead of borrowing Object.prototype.toString.
  • Explaining typeof null as "null is an object" — it isn't; it's a primitive, and the result is a legacy tag-bits bug.

Follow-up questions

  • "How does lodash implement isPlainObject?" — tag check for "[object Object]" plus a prototype walk: the prototype must be Object.prototype or null. Distinguishes {} from class instances.
  • "How would you check for an array specifically?"Array.isArray, and explain the iframe/realm story that motivated it.
  • "Can getType be fooled?" — yes, via Symbol.toStringTag; discuss when that matters (validating untrusted input → prefer structural checks).
  • "Why does typeof document.all === 'undefined'?" — a deliberate spec carve-out (the only falsy object) so ancient if (document.all) browser-sniffing code kept working. Great trivia; better judgment is knowing never to rely on it.
  • "Where does this show up in real code?"deep clone and deep equal both need exact type dispatch before they can recurse; serializers like JSON.stringify are type dispatch all the way down.

  • Serialize values by hand: undefined, functions, cycles, and all the special cases JSON defines.

  • Object.is

    beginner

    The equality that fixes === for NaN and -0: tiny implementation, classic trivia follow-ups.

  • JSON.parse

    advanced

    Write a small recursive-descent parser — the deepest polyfill question in the set.

  • Object.assign

    intermediate

    Copy enumerable own properties across sources, with getter evaluation and null-target errors.