How to Use The TypeScript instanceof Operator
You're debugging a function that processes API responses. TypeScript says everything is fine, but at runtime you get "Cannot read property 'process' of undefined." The issue? Your code assumed an object was an instance of ApiResponse, but it was actually a plain object that just happened to have similar properties.
This is where instanceof becomes essential. While TypeScript's compiler checks types at build time, instanceof verifies actual object types at runtime. In this guide, you'll learn how to use instanceof for runtime type checking, understand when it works (and when it doesn't), and combine it with TypeScript's type narrowing for safer code.
Introduction to TypeScript's instanceof Operator
The instanceof operator checks whether an object was created from a specific class or constructor function. It does this by examining the object's prototype chain, returning true if the constructor's prototype appears anywhere in that chain.
class Animal {}
class Dog extends Animal {}
const dog = new Dog();
console.log(dog instanceof Dog); // true
console.log(dog instanceof Animal); // true - checks the prototype chain
console.log(dog instanceof Object); // true - everything inherits from Object
Here's the key thing to understand: instanceof doesn't check if an object has the right shape or properties. It checks if the object was actually constructed using a specific class or constructor function.
instanceof vs typeof: When to Use Each
You'll often see typeof and instanceof mentioned together, but they serve different purposes. Here's when to reach for each:
// typeof checks primitive types
console.log(typeof 42); // "number"
console.log(typeof "hello"); // "string"
console.log(typeof true); // "boolean"
console.log(typeof undefined); // "undefined"
// instanceof checks class instances
class ApiResponse {}
const response = new ApiResponse();
console.log(response instanceof ApiResponse); // true
console.log(typeof response); // "object" - not very helpful!
Use typeof when you're working with primitives (strings, numbers, booleans). Use instanceof when you need to verify that an object was created from a specific class. This matters for inheritance scenarios where you need to know not just that something is an object, but specifically what kind of object it is.
function processValue(value: unknown) {
// For primitives, use typeof
if (typeof value === "string") {
return value.toUpperCase();
}
// For class instances, use instanceof
if (value instanceof Date) {
return value.toISOString();
}
}
Type Narrowing with instanceof
One of the most powerful features of instanceof in TypeScript is that it acts as a type guard, automatically narrowing types within conditional blocks. TypeScript understands instanceof checks and adjusts the type accordingly.
class Dog {
bark() {
console.log("Woof!");
}
}
class Cat {
meow() {
console.log("Meow!");
}
}
function makeSound(animal: Dog | Cat) {
// TypeScript knows animal could be either Dog or Cat here
if (animal instanceof Dog) {
// Inside this block, TypeScript narrows animal to type Dog
animal.bark(); // ✅ No error - TypeScript knows this is a Dog
} else {
// TypeScript narrows to Cat here (the only other option)
animal.meow(); // ✅ No error - TypeScript knows this is a Cat
}
}
This type narrowing works automatically without any extra type annotations. TypeScript follows the control flow of your code and understands what type is possible in each branch.
This pattern becomes really useful when working with data from external sources or when dealing with objects where type assertion alone isn't sufficient. When combined with TypeScript's check type capabilities, you get both runtime safety and compile-time type checking.
Error Handling with instanceof
Error handling is one of the most common real-world uses for instanceof. Since JavaScript lets you throw anything, instanceof helps you handle different error types appropriately:
class ValidationError extends Error {
constructor(public field: string, message: string) {
super(message);
}
}
class NetworkError extends Error {
constructor(public statusCode: number, message: string) {
super(message);
}
}
async function fetchUserData(userId: string) {
try {
// Some operation that might throw
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new NetworkError(response.status, "Failed to fetch user");
}
return response.json();
} catch (error) {
if (error instanceof NetworkError) {
// Handle network errors - TypeScript knows error.statusCode exists
console.error(`Network error (${error.statusCode}): ${error.message}`);
} else if (error instanceof ValidationError) {
// Handle validation errors - TypeScript knows error.field exists
console.error(`Validation failed for ${error.field}: ${error.message}`);
} else if (error instanceof Error) {
// Handle generic errors
console.error(`Unexpected error: ${error.message}`);
} else {
// Handle non-Error throws
console.error("Unknown error:", error);
}
}
}
This pattern is especially valuable when implementing error handling in Convex functions where different error types require different responses.
Object Literals vs Constructor Instances: A Critical Gotcha
Here's a mistake that trips up many TypeScript developers: instanceof only works with objects created using the new keyword. It fails with plain object literals, even if they have the exact same properties.
class User {
constructor(public name: string, public email: string) {}
greet() {
console.log(`Hello, I'm ${this.name}`);
}
}
// Created with 'new' - instanceof works
const user1 = new User("Alice", "alice@example.com");
console.log(user1 instanceof User); // ✅ true
// Plain object literal - instanceof fails!
const user2 = {
name: "Bob",
email: "bob@example.com",
greet() {
console.log(`Hello, I'm ${this.name}`);
}
};
console.log(user2 instanceof User); // ❌ false - same shape, different constructor
// Type annotation doesn't help
const user3: User = {
name: "Charlie",
email: "charlie@example.com",
greet() {
console.log(`Hello, I'm ${this.name}`);
}
};
console.log(user3 instanceof User); // ❌ Still false!
Why does this happen? Because instanceof checks the prototype chain, not the object's shape. When you create an object with new User(), JavaScript links it to User.prototype. Plain object literals are linked to Object.prototype instead, so instanceof User returns false.
When This Gotcha Appears
This limitation catches developers off guard in a few common scenarios:
// Scenario 1: JSON deserialization
const jsonString = '{"name": "Alice", "email": "alice@example.com"}';
const parsed = JSON.parse(jsonString);
console.log(parsed instanceof User); // false - it's a plain object!
// Scenario 2: API responses
async function fetchUser(id: string) {
const response = await fetch(`/api/users/${id}`);
const data = await response.json(); // Returns plain object
console.log(data instanceof User); // false!
return data;
}
// Solution: Create actual instances
function deserializeUser(data: any): User {
return new User(data.name, data.email);
}
When building web applications with Convex, you'll often work with data from your database, which returns plain objects. If you need instanceof checks, you'll need to explicitly construct class instances from that data.
Alternatives When You Can't Use new
If you're working with plain objects and can't use constructors, you have a few options:
// Option 1: Property checking (duck typing)
function isUser(obj: any): obj is User {
return obj &&
typeof obj.name === 'string' &&
typeof obj.email === 'string' &&
typeof obj.greet === 'function';
}
// Option 2: Use interfaces instead of classes
interface UserData {
name: string;
email: string;
}
function processUser(user: UserData) {
// Works with both class instances and plain objects
console.log(user.name);
}
Working with Built-in JavaScript Types
Beyond custom classes, instanceof works with JavaScript's built-in types. This is useful when you receive data from external sources and need to verify what you're actually dealing with:
function processValue(value: unknown) {
if (value instanceof Array) {
// TypeScript narrows to array type
return value.map(item => String(item).toUpperCase());
}
if (value instanceof Date) {
// TypeScript narrows to Date
return value.toISOString();
}
if (value instanceof RegExp) {
// TypeScript narrows to RegExp
return value.source;
}
if (value instanceof Error) {
// TypeScript narrows to Error
return { message: value.message, stack: value.stack };
}
// Fallback for everything else
return String(value);
}
This approach complements TypeScript's compile-time check type system, filling the gap where static typing ends and runtime verification begins. It's particularly valuable when working with dynamic data from Convex where types may not be known until execution.
Watch Out for Primitive Wrappers
JavaScript has both primitive types and object wrappers for primitives. instanceof only works with the object wrappers, which you'll rarely encounter in practice:
// Primitives - instanceof returns false
const str = "hello";
const num = 42;
const bool = true;
console.log(str instanceof String); // false
console.log(num instanceof Number); // false
console.log(bool instanceof Boolean); // false
// Object wrappers - instanceof returns true (but don't use these!)
const strObj = new String("hello");
const numObj = new Number(42);
const boolObj = new Boolean(true);
console.log(strObj instanceof String); // true
console.log(numObj instanceof Number); // true
console.log(boolObj instanceof Boolean); // true
// For primitives, use typeof instead
console.log(typeof str); // "string"
console.log(typeof num); // "number"
console.log(typeof bool); // "boolean"
You'll almost never create primitive wrappers with new String(), new Number(), etc. in real code. Just remember: for primitives, use typeof, not instanceof.
Checking Class Inheritance with instanceof
One of instanceof's strengths is checking inheritance hierarchies. When an object is created from a subclass, instanceof returns true for both the subclass and all parent classes in the chain:
class Animal {
move() {
console.log("Moving...");
}
}
class Mammal extends Animal {
breathe() {
console.log("Breathing...");
}
}
class Dog extends Mammal {
bark() {
console.log("Woof!");
}
}
const spot = new Dog();
// instanceof checks the entire prototype chain
console.log(spot instanceof Dog); // true - direct class
console.log(spot instanceof Mammal); // true - parent class
console.log(spot instanceof Animal); // true - grandparent class
console.log(spot instanceof Object); // true - all objects inherit from Object
This lets you write functions that handle different levels of specificity in your class hierarchy:
function handleAnimal(animal: Animal) {
// All animals can move
animal.move();
// Only mammals can breathe (in this example)
if (animal instanceof Mammal) {
animal.breathe();
}
// Only dogs can bark
if (animal instanceof Dog) {
animal.bark();
}
}
Unlike typeof, which only reveals basic types like "object" or "string", instanceof traverses the entire prototype chain. This makes it perfect for working with inheritance hierarchies in object-oriented code.
When working with TypeScript projects using Convex, you can use this pattern to implement polymorphic behavior while maintaining type safety—a common need in end-to-end typed applications.
When instanceof Doesn't Work: Interfaces and Type Aliases
A common source of confusion: you can't use instanceof with TypeScript interfaces or type aliases. Interfaces exist only at compile time and are erased during compilation to JavaScript.
interface User {
name: string;
email: string;
}
const user = {
name: "Alice",
email: "alice@example.com"
};
// ❌ Error: 'User' only refers to a type, but is being used as a value here
if (user instanceof User) {
// This won't work!
}
For interface validation, use custom type guards with property checks:
interface User {
name: string;
email: string;
}
function isUser(obj: unknown): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
'name' in obj &&
'email' in obj &&
typeof obj.name === 'string' &&
typeof obj.email === 'string'
);
}
const data: unknown = { name: "Alice", email: "alice@example.com" };
if (isUser(data)) {
// TypeScript narrows data to User type
console.log(data.name); // ✅ Works!
}
This approach leverages discriminated union patterns for reliable type checking and works well when implementing validation in Convex functions, where you need to check object shapes rather than class instances.
For testing these patterns, consider using Convex's testing tools to verify your type checks work as expected.
Debugging: What to Check When instanceof Returns Unexpected Results
When instanceof doesn't behave as expected, here are the most common culprits and how to debug them:
Problem 1: Object Created Without new
class Config {
constructor(public apiKey: string) {}
}
// This looks fine in TypeScript...
const config: Config = {
apiKey: "abc123"
};
// ...but instanceof fails at runtime
console.log(config instanceof Config); // false
// Fix: Use the constructor
const config = new Config("abc123");
console.log(config instanceof Config); // true
Problem 2: Data from JSON Deserialization
class Product {
constructor(public id: number, public name: string) {}
getDisplayName() {
return `#${this.id}: ${this.name}`;
}
}
// API returns JSON, which becomes plain objects
const jsonData = '{"id": 1, "name": "Widget"}';
const parsed = JSON.parse(jsonData);
console.log(parsed instanceof Product); // false - it's just a plain object!
// Fix: Create instances from the parsed data
function deserializeProduct(data: any): Product {
return new Product(data.id, data.name);
}
const product = deserializeProduct(parsed);
console.log(product instanceof Product); // true
product.getDisplayName(); // ✅ Now methods work
Problem 3: Multiple JavaScript Realms
If you're working with iframes or different execution contexts, objects can fail instanceof checks even when they should pass:
// Main window
class MyClass {}
// In an iframe
const iframe = document.querySelector('iframe');
const iframeClass = iframe.contentWindow.MyClass;
const obj = new iframeClass();
console.log(obj instanceof MyClass); // false - different class reference!
This is rare but can happen in complex browser applications. The solution is usually to avoid instanceof across contexts and use property checks instead.
Problem 4: TypeScript Says It's Fine, Runtime Says It's Not
class ApiResponse {
constructor(public data: any) {}
}
// TypeScript is happy with this type annotation
function handleResponse(response: ApiResponse) {
if (response instanceof ApiResponse) {
// This check might fail at runtime!
console.log("Valid response");
}
}
// Someone calls it with a plain object
handleResponse({ data: "something" } as ApiResponse); // Type assertion bypasses safety
// Fix: Always use instanceof when you need runtime verification
function handleResponse(response: unknown) {
if (response instanceof ApiResponse) {
// Now we actually verify at runtime
console.log("Valid response");
} else {
throw new Error("Invalid response type");
}
}
The takeaway? TypeScript's type system and instanceof serve different purposes. TypeScript checks types at compile time; instanceof verifies actual object construction at runtime.
Key Takeaways
The instanceof operator fills a critical gap in TypeScript: it verifies object construction at runtime, while TypeScript's type system only checks types at compile time. Here's what you need to remember:
When to use instanceof:
- Checking if an object was created from a specific class or constructor
- Validating class inheritance hierarchies
- Error handling with different error types
- Working with built-in types like
Array,Date,RegExp, orError
When NOT to use instanceof:
- Checking primitive types (use
typeofinstead) - Validating interfaces or type aliases (use custom type guards)
- Testing plain object literals (they'll always fail, even with matching shapes)
- Checking objects from
JSON.parse()(they're plain objects, not class instances)
The most important rule: instanceof only returns true for objects created with the new keyword. If you're working with plain object literals or JSON data, you'll need custom type guards or property checks instead.
TypeScript's type narrowing understands instanceof checks automatically, giving you both runtime safety and compile-time type checking in one. Use it whenever you need to verify that an object was actually constructed using a specific class, not just that it has the right shape.