Skip to main content

How to Check Class Types with instanceof in JavaScript

Knowing the type of an object at runtime is a fundamental need in JavaScript. You might need to handle different error types in a catch block, validate function arguments, implement polymorphic behavior, or route objects through different processing pipelines. The instanceof operator is JavaScript's primary tool for this, but understanding how it works beneath the surface reveals both its power and its limitations.

This guide covers instanceof from basic usage through its internal prototype chain mechanism, the Symbol.hasInstance hook for custom type checking, the surprisingly powerful Object.prototype.toString technique, and how to build a universal type-checking function that handles every edge case.

The instanceof Operator

The instanceof operator checks whether an object is an instance of a particular class or constructor function. It returns true if the object was created by that class or any class that inherits from it.

Basic Usage

class Animal {
constructor(name) {
this.name = name;
}
}

class Dog extends Animal {
bark() {
return `${this.name} says woof!`;
}
}

class Cat extends Animal {
meow() {
return `${this.name} says meow!`;
}
}

const rex = new Dog("Rex");
const whiskers = new Cat("Whiskers");

console.log(rex instanceof Dog); // true
console.log(rex instanceof Animal); // true (Dog extends Animal)
console.log(rex instanceof Object); // true (everything extends Object)
console.log(rex instanceof Cat); // false (Rex is not a Cat)

console.log(whiskers instanceof Cat); // true
console.log(whiskers instanceof Animal); // true
console.log(whiskers instanceof Dog); // false

Works with Constructor Functions Too

instanceof is not limited to class syntax. It works with traditional constructor functions:

function Vehicle(type) {
this.type = type;
}

function Car(brand) {
Vehicle.call(this, "car");
this.brand = brand;
}

Car.prototype = Object.create(Vehicle.prototype);
Car.prototype.constructor = Car;

const tesla = new Car("Tesla");

console.log(tesla instanceof Car); // true
console.log(tesla instanceof Vehicle); // true
console.log(tesla instanceof Object); // true

Works with Built-In Types

console.log([] instanceof Array);                   // true
console.log([] instanceof Object); // true
console.log({} instanceof Object); // true
console.log(/regex/ instanceof RegExp); // true
console.log(new Date() instanceof Date); // true
console.log(new Map() instanceof Map); // true
console.log(new Set() instanceof Set); // true

// Functions are objects too
console.log((function(){}) instanceof Function); // true
console.log((function(){}) instanceof Object); // true

// Error types
console.log(new Error() instanceof Error); // true
console.log(new TypeError() instanceof TypeError); // true
console.log(new TypeError() instanceof Error); // true (TypeError extends Error)

instanceof Does NOT Work with Primitives

Primitives are not objects, so instanceof always returns false for them:

console.log("hello" instanceof String);             // false
console.log(42 instanceof Number); // false
console.log(true instanceof Boolean); // false
console.log(Symbol() instanceof Symbol); // false
console.log(42n instanceof BigInt); // false

// But wrapper objects DO work
console.log(new String("hello") instanceof String); // true
console.log(new Number(42) instanceof Number); // true

This is an important limitation. instanceof cannot check if a value is a string, number, or boolean primitive. For those, use typeof:

console.log(typeof "hello" === "string");   // true
console.log(typeof 42 === "number"); // true
console.log(typeof true === "boolean"); // true

Practical Example: Error Handling

class AppError extends Error {
constructor(message, code) {
super(message);
this.name = "AppError";
this.code = code;
}
}

class ValidationError extends AppError {
constructor(message, field) {
super(message, "VALIDATION_ERROR");
this.name = "ValidationError";
this.field = field;
}
}

class NotFoundError extends AppError {
constructor(resource) {
super(`${resource} not found`, "NOT_FOUND");
this.name = "NotFoundError";
}
}

function handleError(error) {
// Check from most specific to least specific
if (error instanceof ValidationError) {
console.log(`Validation failed on "${error.field}": ${error.message}`);
} else if (error instanceof NotFoundError) {
console.log(`Resource error: ${error.message}`);
} else if (error instanceof AppError) {
console.log(`Application error [${error.code}]: ${error.message}`);
} else if (error instanceof Error) {
console.log(`Unexpected error: ${error.message}`);
} else {
console.log(`Unknown thrown value: ${error}`);
}
}

handleError(new ValidationError("Email is invalid", "email"));
// Validation failed on "email": Email is invalid

handleError(new NotFoundError("User"));
// Resource error: User not found

handleError(new Error("Something broke"));
// Unexpected error: Something broke
tip

When using instanceof with an inheritance chain, always check the most specific type first. Since a ValidationError is also an AppError and an Error, checking error instanceof Error first would match all of them, preventing the more specific checks from running.

How instanceof Works: The Prototype Chain Check

instanceof does not check which constructor created an object. It walks the prototype chain of the object, looking for the prototype property of the constructor on the right-hand side.

The Algorithm

obj instanceof Constructor performs this check:

  1. Get Constructor.prototype
  2. Get obj.__proto__ (i.e., Object.getPrototypeOf(obj))
  3. Are they the same object? If yes, return true
  4. If not, follow the chain: get obj.__proto__.__proto__
  5. Are they the same? If yes, return true
  6. Keep going until null is reached (end of chain)
  7. If null is reached without a match, return false

Visualizing the Check

class A {}
class B extends A {}
class C extends B {}

const c = new C();

// c instanceof C
// Check: c.__proto__ === C.prototype? YES → true
console.log(c instanceof C); // true

// c instanceof B
// Check: c.__proto__ === B.prototype? NO
// Check: c.__proto__.__proto__ === B.prototype? YES → true
console.log(c instanceof B); // true

// c instanceof A
// Check: c.__proto__ === A.prototype? NO
// Check: c.__proto__.__proto__ === A.prototype? NO
// Check: c.__proto__.__proto__.__proto__ === A.prototype? YES → true
console.log(c instanceof A); // true
c → C.prototype → B.prototype → A.prototype → Object.prototype → null
↑ match C ↑ match B ↑ match A ↑ match Object

Manually Implementing instanceof

This helps solidify understanding:

function myInstanceOf(obj, Constructor) {
// Handle edge cases
if (obj === null || obj === undefined) return false;
if (typeof obj !== "object" && typeof obj !== "function") return false;

const target = Constructor.prototype;
let current = Object.getPrototypeOf(obj);

while (current !== null) {
if (current === target) return true;
current = Object.getPrototypeOf(current);
}

return false;
}

class Animal {}
class Dog extends Animal {}

const rex = new Dog();

console.log(myInstanceOf(rex, Dog)); // true
console.log(myInstanceOf(rex, Animal)); // true
console.log(myInstanceOf(rex, Object)); // true
console.log(myInstanceOf(rex, Array)); // false
console.log(myInstanceOf("hello", String)); // false (primitive)

Consequence: Modifying the Prototype Chain Affects instanceof

Since instanceof only checks the prototype chain, you can manipulate it:

class Rabbit {}

const rabbit = new Rabbit();
console.log(rabbit instanceof Rabbit); // true

// Remove the link by changing the prototype
Rabbit.prototype = {};

// Now the old prototype is no longer Rabbit.prototype
console.log(rabbit instanceof Rabbit); // false!
// rabbit's __proto__ still points to the OLD Rabbit.prototype object,
// but Rabbit.prototype is now a different object

This is why you should never replace a constructor's prototype object after creating instances.

instanceof and Cross-Realm Objects

A tricky limitation of instanceof is that it fails across different JavaScript realms (iframes, different Node.js vm contexts). Each realm has its own set of built-in constructors:

// In a browser with an iframe:
const iframe = document.createElement("iframe");
document.body.appendChild(iframe);

const iframeArray = iframe.contentWindow.Array;

const arr = new iframeArray(1, 2, 3);

console.log(arr instanceof iframeArray); // true
console.log(arr instanceof Array); // false!
// The iframe's Array.prototype is a different object from the main page's Array.prototype

// This is why Array.isArray exists:
console.log(Array.isArray(arr)); // true (works across realms)

Symbol.hasInstance: Custom instanceof Logic

You can customize how instanceof behaves for your classes by implementing the Symbol.hasInstance static method. This is a well-known Symbol that instanceof checks before performing its default prototype chain walk.

Basic Customization

class EvenNumber {
static [Symbol.hasInstance](value) {
return typeof value === "number" && value % 2 === 0;
}
}

console.log(2 instanceof EvenNumber); // true
console.log(4 instanceof EvenNumber); // true
console.log(3 instanceof EvenNumber); // false
console.log("4" instanceof EvenNumber); // false (not a number)
console.log(2.5 instanceof EvenNumber); // false (not even)

Notice that Symbol.hasInstance even works with primitives, which normally fail with instanceof. The custom logic completely replaces the default prototype chain check.

Validation with Symbol.hasInstance

class ValidEmail {
static [Symbol.hasInstance](value) {
return typeof value === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}
}

class PositiveInteger {
static [Symbol.hasInstance](value) {
return typeof value === "number" && Number.isInteger(value) && value > 0;
}
}

class NonEmptyString {
static [Symbol.hasInstance](value) {
return typeof value === "string" && value.trim().length > 0;
}
}

console.log("alice@example.com" instanceof ValidEmail); // true
console.log("not-an-email" instanceof ValidEmail); // false

console.log(42 instanceof PositiveInteger); // true
console.log(-5 instanceof PositiveInteger); // false
console.log(3.14 instanceof PositiveInteger); // false

console.log("hello" instanceof NonEmptyString); // true
console.log(" " instanceof NonEmptyString); // false
console.log("" instanceof NonEmptyString); // false

Duck Typing with Symbol.hasInstance

You can create "interfaces" that check for the presence of specific methods or properties:

class Iterable {
static [Symbol.hasInstance](obj) {
return obj !== null &&
obj !== undefined &&
typeof obj[Symbol.iterator] === "function";
}
}

console.log([1, 2, 3] instanceof Iterable); // true (arrays are iterable)
console.log("hello" instanceof Iterable); // true (strings are iterable)
console.log(new Map() instanceof Iterable); // true (maps are iterable)
console.log(new Set() instanceof Iterable); // true (sets are iterable)
console.log({ a: 1 } instanceof Iterable); // false (plain objects are not)
console.log(42 instanceof Iterable); // false (numbers are not)

class Serializable {
static [Symbol.hasInstance](obj) {
return obj !== null &&
obj !== undefined &&
typeof obj.toJSON === "function";
}
}

class User {
constructor(name) { this.name = name; }
toJSON() { return { name: this.name }; }
}

console.log(new User("Alice") instanceof Serializable); // true
console.log({ a: 1 } instanceof Serializable); // false
console.log(new Date() instanceof Serializable); // true (Date has toJSON!)

Combining Default and Custom Behavior

You can extend the default instanceof behavior rather than replacing it entirely:

class FlexibleArray extends Array {
static [Symbol.hasInstance](instance) {
// Accept regular arrays AND array-like objects
if (Array.isArray(instance)) return true;
if (instance && typeof instance.length === "number" && instance.length >= 0) {
return Number.isInteger(instance.length);
}
return false;
}
}

console.log([1, 2, 3] instanceof FlexibleArray); // true
console.log(new FlexibleArray() instanceof FlexibleArray); // true
console.log({ length: 3, 0: "a", 1: "b", 2: "c" } instanceof FlexibleArray); // true
console.log("hello" instanceof FlexibleArray); // true (strings have length)
console.log({ length: -1 } instanceof FlexibleArray); // false
warning

Use Symbol.hasInstance judiciously. Overriding instanceof behavior can confuse other developers who expect the standard prototype-chain check. It is best used for utility classes that serve as type validators (like EvenNumber or ValidEmail) rather than for classes that you actually instantiate with new.

Object.prototype.toString for Type Checking

JavaScript has a hidden gem for type checking: Object.prototype.toString. When called on any value, it returns a string in the format "[object Type]", where Type is the internal class of the value.

Basic Usage

The key is to use call to invoke toString with the value as this, because most objects override toString with their own implementation:

const toString = Object.prototype.toString;

// Primitives
console.log(toString.call(42)); // "[object Number]"
console.log(toString.call("hello")); // "[object String]"
console.log(toString.call(true)); // "[object Boolean]"
console.log(toString.call(undefined)); // "[object Undefined]"
console.log(toString.call(null)); // "[object Null]"
console.log(toString.call(Symbol())); // "[object Symbol]"
console.log(toString.call(42n)); // "[object BigInt]"

// Objects
console.log(toString.call({})); // "[object Object]"
console.log(toString.call([])); // "[object Array]"
console.log(toString.call(new Map())); // "[object Map]"
console.log(toString.call(new Set())); // "[object Set]"
console.log(toString.call(new Date())); // "[object Date]"
console.log(toString.call(/regex/)); // "[object RegExp]"
console.log(toString.call(new Error())); // "[object Error]"
console.log(toString.call(function(){})); // "[object Function]"
console.log(toString.call(function*(){})); // "[object GeneratorFunction]"

// Special objects
console.log(toString.call(Math)); // "[object Math]"
console.log(toString.call(JSON)); // "[object JSON]"
console.log(toString.call(new Promise(()=>{}))); // "[object Promise]"
console.log(toString.call(new WeakMap())); // "[object WeakMap]"
console.log(toString.call(new WeakSet())); // "[object WeakSet]"
console.log(toString.call(new ArrayBuffer(8))); // "[object ArrayBuffer]"
console.log(toString.call(new Int32Array())); // "[object Int32Array]"

Why This Is Better Than typeof

typeof has well-known limitations:

console.log(typeof []);                       // "object" (not helpful)
console.log(typeof null); // "object" (infamous bug)
console.log(typeof new Date()); // "object" (not helpful)
console.log(typeof /regex/); // "object" (not helpful)

// Object.prototype.toString distinguishes all of these:
const toString = Object.prototype.toString;
console.log(toString.call([])); // "[object Array]"
console.log(toString.call(null)); // "[object Null]"
console.log(toString.call(new Date())); // "[object Date]"
console.log(toString.call(/regex/)); // "[object RegExp]"

Customizing toString with Symbol.toStringTag

You can control what Object.prototype.toString returns for your own classes by defining Symbol.toStringTag:

class User {
constructor(name) {
this.name = name;
}

get [Symbol.toStringTag]() {
return "User";
}
}

const alice = new User("Alice");
console.log(Object.prototype.toString.call(alice)); // "[object User]"

// Without Symbol.toStringTag, it would show "[object Object]"
class Plain {}
console.log(Object.prototype.toString.call(new Plain())); // "[object Object]"

Symbol.toStringTag can be a property or a getter. Using a getter allows dynamic values:

class Connection {
#status = "disconnected";

get [Symbol.toStringTag]() {
return `Connection(${this.#status})`;
}

connect() {
this.#status = "connected";
}

disconnect() {
this.#status = "disconnected";
}
}

const conn = new Connection();
console.log(Object.prototype.toString.call(conn));
// "[object Connection(disconnected)]"

conn.connect();
console.log(Object.prototype.toString.call(conn));
// "[object Connection(connected)]"

Built-In Symbol.toStringTag Values

Several built-in objects already define Symbol.toStringTag:

console.log(Map.prototype[Symbol.toStringTag]);         // "Map"
console.log(Set.prototype[Symbol.toStringTag]); // "Set"
console.log(WeakMap.prototype[Symbol.toStringTag]); // "WeakMap"
console.log(WeakSet.prototype[Symbol.toStringTag]); // "WeakSet"
console.log(ArrayBuffer.prototype[Symbol.toStringTag]); // "ArrayBuffer"

// Generators
function* gen() {}
console.log(gen()[Symbol.toStringTag]); // "Generator"

// Module namespace objects in ES modules also have it
// import * as mod from './module.js';
// mod[Symbol.toStringTag] → "Module"

Extracting Just the Type Name

For practical use, you often want just the type name without the [object ...] wrapper:

function getType(value) {
return Object.prototype.toString.call(value).slice(8, -1);
}

console.log(getType(42)); // "Number"
console.log(getType("hello")); // "String"
console.log(getType(true)); // "Boolean"
console.log(getType(null)); // "Null"
console.log(getType(undefined)); // "Undefined"
console.log(getType([])); // "Array"
console.log(getType({})); // "Object"
console.log(getType(new Map())); // "Map"
console.log(getType(new Set())); // "Set"
console.log(getType(new Date())); // "Date"
console.log(getType(/regex/)); // "RegExp"
console.log(getType(new Error())); // "Error"
console.log(getType(() => {})); // "Function"
console.log(getType(Symbol())); // "Symbol"
console.log(getType(42n)); // "BigInt"

The Universal Type-Checking Function

Combining typeof, instanceof, and Object.prototype.toString, we can build a comprehensive type-checking utility:

A Complete Type Checker

function typeOf(value) {
// Handle null explicitly (typeof null === "object" is a bug)
if (value === null) return "null";

// Handle undefined
if (value === undefined) return "undefined";

// For primitives, typeof is sufficient and fast
const primitiveType = typeof value;
if (primitiveType !== "object" && primitiveType !== "function") {
return primitiveType; // "number", "string", "boolean", "symbol", "bigint"
}

// For objects and functions, use Object.prototype.toString for precision
const tag = Object.prototype.toString.call(value).slice(8, -1);

// Normalize common cases
return tag;
}

// Primitives
console.log(typeOf(42)); // "number"
console.log(typeOf("hello")); // "string"
console.log(typeOf(true)); // "boolean"
console.log(typeOf(null)); // "null"
console.log(typeOf(undefined)); // "undefined"
console.log(typeOf(Symbol())); // "symbol"
console.log(typeOf(42n)); // "bigint"

// Objects
console.log(typeOf({})); // "Object"
console.log(typeOf([])); // "Array"
console.log(typeOf(new Map())); // "Map"
console.log(typeOf(new Set())); // "Set"
console.log(typeOf(new Date())); // "Date"
console.log(typeOf(/regex/)); // "RegExp"
console.log(typeOf(new Error())); // "Error"
console.log(typeOf(new Promise(()=>{}))); // "Promise"
console.log(typeOf(() => {})); // "Function"
console.log(typeOf(function*(){})); // "GeneratorFunction"

Type-Checking Utility Library

class TypeChecker {
static #toString = Object.prototype.toString;

static getTag(value) {
return this.#toString.call(value).slice(8, -1);
}

// Primitive checks
static isString(value) { return typeof value === "string"; }
static isNumber(value) { return typeof value === "number" && !isNaN(value); }
static isBoolean(value) { return typeof value === "boolean"; }
static isSymbol(value) { return typeof value === "symbol"; }
static isBigInt(value) { return typeof value === "bigint"; }
static isUndefined(value) { return value === undefined; }
static isNull(value) { return value === null; }
static isNullish(value) { return value == null; } // null or undefined

// Object checks
static isObject(value) {
return value !== null && (typeof value === "object" || typeof value === "function");
}

static isPlainObject(value) {
if (this.getTag(value) !== "Object") return false;
const proto = Object.getPrototypeOf(value);
return proto === Object.prototype || proto === null;
}

static isArray(value) { return Array.isArray(value); }
static isFunction(value) { return typeof value === "function"; }
static isDate(value) { return this.getTag(value) === "Date"; }
static isRegExp(value) { return this.getTag(value) === "RegExp"; }
static isError(value) { return value instanceof Error; }
static isMap(value) { return this.getTag(value) === "Map"; }
static isSet(value) { return this.getTag(value) === "Set"; }
static isPromise(value) { return this.getTag(value) === "Promise"; }
static isWeakMap(value) { return this.getTag(value) === "WeakMap"; }
static isWeakSet(value) { return this.getTag(value) === "WeakSet"; }

// Iterable check
static isIterable(value) {
return value !== null &&
value !== undefined &&
typeof value[Symbol.iterator] === "function";
}

// Array-like check
static isArrayLike(value) {
if (typeof value === "string") return true;
if (!this.isObject(value)) return false;
return typeof value.length === "number" &&
value.length >= 0 &&
Number.isInteger(value.length);
}

// Primitive check (any primitive)
static isPrimitive(value) {
return value === null || (typeof value !== "object" && typeof value !== "function");
}
}

// Usage
console.log(TypeChecker.isPlainObject({})); // true
console.log(TypeChecker.isPlainObject(new Date())); // false
console.log(TypeChecker.isPlainObject([])); // false
console.log(TypeChecker.isPlainObject(Object.create(null))); // true

console.log(TypeChecker.isIterable([1, 2])); // true
console.log(TypeChecker.isIterable("hello")); // true
console.log(TypeChecker.isIterable({})); // false

console.log(TypeChecker.isArrayLike({ length: 3, 0: "a", 1: "b", 2: "c" })); // true
console.log(TypeChecker.isArrayLike(42)); // false

console.log(TypeChecker.isPrimitive(42)); // true
console.log(TypeChecker.isPrimitive("hello")); // true
console.log(TypeChecker.isPrimitive(null)); // true
console.log(TypeChecker.isPrimitive([])); // false
console.log(TypeChecker.isPrimitive({})); // false

Comparison of Type-Checking Approaches

TechniquePrimitivesBuilt-in ObjectsCustom ClassesCross-RealmCustom Logic
typeofGood (except null)Poor (most return "object")Returns "object"WorksNo
instanceofDoes not workGoodGoodFailsVia Symbol.hasInstance
Object.prototype.toStringExcellentExcellentNeeds Symbol.toStringTagWorksVia Symbol.toStringTag
Array.isArrayN/AOnly for arraysN/AWorksNo
constructor checkNoFragileFragileFailsNo

Quick Decision Guide

// Need to check a primitive type?
typeof value === "string" // Use typeof

// Need to check if something is an array?
Array.isArray(value) // Use Array.isArray (cross-realm safe)

// Need to check class hierarchy?
value instanceof MyClass // Use instanceof

// Need to distinguish object types (Date, RegExp, Map, etc.)?
Object.prototype.toString.call(value) // Use toString

// Need to check for null or undefined?
value == null // Checks both null and undefined
value === null // Only null
value === undefined // Only undefined

Summary

ConceptKey Takeaway
instanceofChecks if an object's prototype chain includes Constructor.prototype
Prototype chain walkinstanceof follows __proto__ links up to null, checking each against Constructor.prototype
Does not work with primitives42 instanceof Number is false; use typeof for primitives
Cross-realm limitationinstanceof fails across iframes/contexts because each realm has separate built-in constructors
Symbol.hasInstanceStatic method that overrides instanceof behavior; enables custom type-checking logic
Object.prototype.toStringReturns "[object Type]" for any value; works with primitives and across realms
Symbol.toStringTagCustomizes the tag returned by Object.prototype.toString for your classes
typeofBest for primitive checks; returns "object" for most objects and "object" for null
Array.isArrayThe reliable way to check for arrays, including cross-realm
Best practiceCombine typeof for primitives, instanceof for class hierarchies, and toString for precise object type identification

Type checking in JavaScript requires different tools for different situations. No single approach handles every case perfectly. Understanding when to use typeof, instanceof, Array.isArray, and Object.prototype.toString gives you a complete toolkit for reliably identifying values at runtime, regardless of where they come from or how they were created.