JSON.stringify
advancedSerialize values by hand: undefined, functions, cycles, and all the special cases JSON defines.
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.
typeofsaysnullis an"object", arrays are"object"s, andNaNis a"number". Build agetType(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.
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 languageinstanceof and .toString()A first draft usually chains instanceof Date, instanceof RegExp, or calls value.toString(). Three failure modes worth naming:
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.value.toString() is user-overridable — any object can lie about itself, and Object.create(null) has no toString at all, so the check throws.instanceof checks doesn't scale — every new type is another branch.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.)
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();
}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.getType(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 variables — typeof 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.console.log(
typeof null,
typeof NaN,
typeof [],
typeof (() => {})
);instanceof ladder and never mentioning realms.isNaN instead of Number.isNaN (isNaN("foo") is true — coercion strikes again).value.toString() instead of borrowing Object.prototype.toString.typeof null as "null is an object" — it isn't; it's a primitive, and the result is a legacy tag-bits bug.isPlainObject?" — tag check for "[object Object]" plus a prototype walk: the prototype must be Object.prototype or null. Distinguishes {} from class instances.Array.isArray, and explain the iframe/realm story that motivated it.getType be fooled?" — yes, via Symbol.toStringTag; discuss when that matters (validating untrusted input → prefer structural checks).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.Serialize values by hand: undefined, functions, cycles, and all the special cases JSON defines.
The equality that fixes === for NaN and -0: tiny implementation, classic trivia follow-ups.
Write a small recursive-descent parser — the deepest polyfill question in the set.
Copy enumerable own properties across sources, with getter evaluation and null-target errors.