Typeof, instanceof, and reliable type checking
`typeof` returns a primitive type string but is wrong for null ("object") and arrays. `instanceof` checks the prototype chain but fails across realms (iframes). The reliable cross-realm check is `Object.prototype.toString.call(x)` → "[object Array]" etc. For primitives use `typeof`; for arrays `Array.isArray`; for null `=== null`; for plain objects there's no perfect check.
JavaScript's type system has more rough edges than most realize. The four common checks each have failure modes.
typeof
typeof undefined // "undefined"
typeof "x" // "string"
typeof 42 // "number"
typeof 42n // "bigint"
typeof true // "boolean"
typeof Symbol() // "symbol"
typeof (() => {}) // "function"
typeof {} // "object"
typeof [] // "object" ← surprising
typeof null // "object" ← historical bugUseful for primitives. Useless for distinguishing arrays / null / plain objects.
instanceof
[] instanceof Array // true
new Date() instanceof Date // trueWalks the prototype chain. Two failure modes:
- Cross-realm — arrays from another iframe / Worker fail
instanceof Arraybecause their prototype is a differentArray.prototype. - Custom
Symbol.hasInstance— classes can override howinstanceofdecides.
Array.isArray
Array.isArray([]) // true
Array.isArray(iframeArray) // true — works cross-realmThe reliable check for arrays. Always prefer this over instanceof Array.
Object.prototype.toString
Object.prototype.toString.call(undefined) // "[object Undefined]"
Object.prototype.toString.call(null) // "[object Null]"
Object.prototype.toString.call([]) // "[object Array]"
Object.prototype.toString.call(new Date()) // "[object Date]"
Object.prototype.toString.call(/x/) // "[object RegExp]"
Object.prototype.toString.call(new Map()) // "[object Map]"The closest thing to a "true class" check, and it's cross-realm safe.
Plain object?
There's no perfect check. Common heuristics:
function isPlainObject(v) {
if (v === null || typeof v !== "object") return false;
const p = Object.getPrototypeOf(v);
return p === null || p === Object.prototype;
}Works for most cases — {} and Object.create(null) return true; class instances, arrays, Maps return false.
null check
Just x === null. Don't use typeof here.
NaN
Number.isNaN(x) — not isNaN() (which coerces). Or x !== x (only NaN is not equal to itself).
Function check
typeof fn === "function". Catches arrow, regular, async, class constructors (which are also functions).
In TypeScript
Most of this disappears with proper types. But user input, JSON parse, third-party libs, postMessage — all still need runtime guards. Use Zod / Valibot to validate at boundaries instead of hand-rolling.
Decision table
| Want to check | Use |
|---|---|
| Primitive type | typeof |
| null | === null |
| Array | Array.isArray |
| Plain object | prototype check above |
| Date / Map / RegExp / specific built-in | Object.prototype.toString.call or instanceof (same realm) |
| External / boundary data | Zod / Valibot schema |
| NaN | Number.isNaN |
Interview framing
"typeof for primitives — it's wrong about null (returns 'object') and lumps arrays in with objects. instanceof walks the prototype chain but fails across realms — iframes have their own Array.prototype. Array.isArray is the cross-realm-safe array check. For built-ins like Date/Map/RegExp, Object.prototype.toString.call(x) is the reliable check. Plain object has no perfect check — I use the prototype-equals-Object.prototype heuristic. At system boundaries (JSON, postMessage, user input), I use Zod to validate instead of hand-rolling type checks."
Follow-up questions
- •Why does typeof null return 'object'?
- •When have you been bitten by cross-realm instanceof?
- •How would you implement deepEqual?
Common mistakes
- •Using typeof to detect arrays.
- •Using instanceof in a multi-realm app (iframes, workers).
- •Hand-rolling type validation at API boundaries instead of using a schema.
Performance considerations
- •All checks are O(1). Object.prototype.toString.call is slightly slower than typeof but negligible. Heavy validation goes through schema libraries (Zod) which cache parsers.
Edge cases
- •typeof of an undeclared variable returns 'undefined' without throwing.
- •Symbol.hasInstance can lie about instanceof.
- •BigInt vs Number — different types, no implicit conversion.
Real-world examples
- •Zod / Yup / Valibot in API layers, lodash's isPlainObject / isArray, browser polyfill code, library type guards.