I’m confused about how Javascript typeof behaves with different values and objects. In my code, some variables return unexpected types like object or function when I use typeof, and it’s breaking my type checks. Can someone explain how typeof actually works under the hood and the best practices for using it reliably in real-world code?
Short version. typeof in JS has a bunch of historical quirks. If you treat it as a precise type system, it bites you.
Core outputs of typeof:
- Primitives
- typeof 123 → ‘number’
- typeof NaN → ‘number’
- typeof ‘hi’ → ‘string’
- typeof true → ‘boolean’
- typeof undefined → ‘undefined’
- typeof 10n → ‘bigint’
- typeof Symbol() → ‘symbol’
- Functions
- typeof function () {} → ‘function’
- typeof class Foo {} → ‘function’
Functions are objects, but typeof gives a special string ‘function’. That is by design so you can do:
if (typeof fn === ‘function’) { … }
- Objects
- typeof {} → ‘object’
- typeof → ‘object’
- typeof new Date() → ‘object’
- typeof /regex/ → ‘object’
- typeof new Map() → ‘object’
- typeof null → ‘object’ ← famous bug
Historically typeof null should have been ‘null’, but the spec kept ‘object’ for backward compatibility. So your type checks break if you expect typeof null to help.
Safer type checks:
-
Handle null explicitly
if (value === null) {
// null
} else if (Array.isArray(value)) {
// array
} else if (typeof value === ‘object’) {
// plain object or other object types
} -
Arrays
typeof === ‘object’, so use:
Array.isArray(value) -
Functions
Check with:
typeof value === ‘function’ -
Better object type detection using Object.prototype.toString
You get a more specific tag:
Object.prototype.toString.call(value)
Examples:
Object.prototype.toString.call() → ‘[object Array]’
Object.prototype.toString.call({}) → ‘[object Object]’
Object.prototype.toString.call(null) → ‘[object Null]’
Object.prototype.toString.call(undefined) → ‘[object Undefined]’
Object.prototype.toString.call(new Date()) → ‘[object Date]’
Helper:
function getTag(v) {
return Object.prototype.toString.call(v).slice(8, -1);
}
getTag() → ‘Array’
getTag(null) → ‘Null’
getTag(new Map()) → ‘Map’
- Typical robust type helper
function typeOf(v) {
if (v === null) return ‘null’;
if (Array.isArray(v)) return ‘array’;
return typeof v;
}
- Why your checks break
Common issues:
- Using typeof value === ‘object’ and expecting no null:
if (typeof v === ‘object’) { // fires for null too
}
Fix:
if (v !== null && typeof v === ‘object’) { … }
- Trying to distinguish array vs object with typeof:
typeof {} → ‘object’
typeof → ‘object’
Use Array.isArray instead.
- Quick rules of thumb
- For primitives, typeof is fine.
- For functions, typeof v === ‘function’ is fine.
- For arrays, use Array.isArray.
- For null, check v === null.
- For detailed kind of object, use Object.prototype.toString.call.
If you post some specific snippet that surprises you, people can point at the exact quirk.
typeof looks like a “type system,” but it’s really more like a coarse classifier that got frozen in the 90s, bugs and all. @cazadordeestrellas already covered the main quirks, so I’ll fill in some gaps and focus on how to actually use it without going insane.
1. Think of typeof as a first-pass filter, not a full type check
In practice, I treat typeof like this:
- “Is this a primitive?”
- “Is this callable?”
- “Is this some kind of object?”
That’s it. If you try to distinguish “array vs plain object vs date vs regex” with typeof, it will betray you.
Example of using it as a filter:
function isPrimitive(v) {
return v === null || (typeof v !== 'object' && typeof v !== 'function');
}
This works despite typeof null === 'object' because we special-case null.
2. typeof is great for guarding code paths
Where typeof actually shines:
if (typeof value === 'function') {
value(); // safe-ish, at least it's callable
}
if (typeof value === 'string') {
// safe to call string methods
console.log(value.toUpperCase());
}
Instead of using typeof to identify all possible types, use it to gate operations that only make sense for a specific broad category.
Bad pattern:
if (typeof v === 'object') {
// assume it's a plain object
}
Better:
if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
// probably a non-array object (plain or custom)
}
Still not perfect, but at least you’re not accidentally treating null as an object.
3. Distinguish your objects from “any random object”
One thing that trips people up: typeof tells you JS-level categories, not your domain types.
If you want to know “is this one of my User objects?” or “is this thing shaped like { id, name }?”, you’re already outside typeof territory.
A few patterns that work better:
a) Use instanceof for classes
class User {}
const u = new User();
u instanceof User; // true
typeof u; // 'object', not useful for distinguishing
Caveats: instanceof can break across realms/iframes, but for most app code it’s fine.
b) Use “duck typing” / structural checks
function isUserLike(v) {
return v !== null
&& typeof v === 'object'
&& 'id' in v
&& typeof v.id === 'number'
&& typeof v.name === 'string';
}
This is where typeof is still useful: to check the fields of an object, not the object itself.
4. Why you see object and function “randomly”
Some concrete “WTF”s that commonly show up:
typeof (() => {}) // 'function'
typeof class A {} // 'function'
typeof new Date() // 'object'
typeof /abc/ // 'object'
typeof Promise.resolve() // 'object'
typeof null // 'object'
Patterns:
- Anything callable at the language level is
function, even classes. - Everything “reference-y” that is not a function is
object, lumped together. nullis just collateral damage from early engine design.
So if your type checks look like:
if (typeof v === 'object') {
// do something
}
you’re unintentionally including:
null- arrays
- dates
- maps
- sets
- your own classes
- promises
- regexes
- etc.
That’s why stuff “breaks.” typeof is doing exactly what the spec says, it’s just not what your app logic actually wanted.
5. If you really want more detail, pick one strategy and stick to it
@cazadordeestrellas mentioned Object.prototype.toString.call. I actually think most people should either:
- Stick with a simple layered strategy:
function kindOf(v) {
if (v === null) return 'null';
if (Array.isArray(v)) return 'array';
return typeof v; // 'string' | 'number' | 'boolean' | 'undefined' | 'bigint' | 'symbol' | 'function' | 'object'
}
Then write your app logic around those 8 + 2 categories.
or
- Go all in on the tag-based approach:
function tag(v) {
return Object.prototype.toString.call(v).slice(8, -1);
}
// 'Array', 'Object', 'Date', 'Set', 'Map', 'RegExp', 'Null', 'Undefined', 'Number', etc.
Personally I think people overuse the fancy toString trick when they actually only need 3 branches:
- primitives
- callable
- object-ish stuff
6. How to stop your current checks from breaking
Typical minimal repairs:
// 1) Guard null when checking objects
if (v !== null && typeof v === 'object') {
// safe 'object-ish' zone
}
// 2) Distinguish array vs not
if (Array.isArray(v)) {
// array
} else if (v !== null && typeof v === 'object') {
// non-array object
}
// 3) Functions
if (typeof v === 'function') {
// callable
}
Once you rewrite your checks like that, the “unexpected object/function results” stop being surprising and just become part of the flow.
TL;DR: treat typeof as a blunt instrument. Great for catching primitives and functions, useless for fine-grained object types. For anything more specific, layer on null checks, Array.isArray, instanceof, or structural checks, instead of trying to force typeof into a real type system.