9.3 instanceof Narrowing: Checking Class Instances
Right, let’s talk about instanceof. This is one of those operators that feels almost magical when you first see it, but its magic is firmly rooted in the dusty, well-trodden ground of JavaScript’s prototype chain. It’s your go-to tool when you need to ask an object, “Hey, who built you?”
In its simplest form, instanceof checks if an object is an instance of a specific class (or a constructor function, for you old-schoolers). It does this by walking up the object’s prototype chain to see if it can find the prototype property of the constructor you’re checking against. If it finds it, you get a true. Simple, right? Well, mostly.
How It Works Under the Hood
Don’t just take my word for it; let’s see the mechanic in action. When you write myValue instanceof MyClass, the JavaScript engine is essentially doing this:
function isInstanceOf(value, constructor) {
let proto = Object.getPrototypeOf(value);
while (proto !== null) {
if (proto === constructor.prototype) {
return true;
}
proto = Object.getPrototypeOf(proto);
}
return false;
}
It’s a recursive prototype walk. This is crucial to understand because it explains the behavior—and the pitfalls.
class Vehicle {
drive() { console.log("Vroom vroom"); }
}
class Car extends Vehicle {
honk() { console.log("Beep beep!"); }
}
const myCar = new Car();
// Both of these are true because the Car prototype
// inherits from the Vehicle prototype.
console.log(myCar instanceof Car); // true
console.log(myCar instanceof Vehicle); // true
console.log(myCar instanceof Object); // true (sigh, always this)
// And this is where TypeScript narrows the type.
if (myCar instanceof Vehicle) {
myCar.drive(); // TypeScript now knows myCar is a Vehicle
// myCar.honk(); // <-- Error! Property 'honk' does not exist on type 'Vehicle'.
}
See? TypeScript uses this runtime check to narrow the type within that if block. It’s not guessing; it’s using the same logic JavaScript does. If instanceof returns true, the type must be compatible with that class.
The Big, Glaring Pitfall: Cross-Realm Values
Here’s the part where the designers’ choice to tie this solely to the prototype chain becomes a genuine headache. instanceof fails spectacularly with values from different JavaScript execution contexts. Think iframes, node:vm modules, or different Node.js vm contexts. Each realm has its own global object, and therefore its own set of constructor functions.
// Imagine 'someValue' comes from an iframe
const iframe = document.createElement('iframe');
document.body.appendChild(iframe);
const iframeArray = iframe.contentWindow.Array;
const myArray = [1, 2, 3];
const theirArray = new iframeArray(1, 2, 3);
console.log(myArray instanceof Array); // true (obviously)
console.log(theirArray instanceof Array); // false (wait, what?)
console.log(theirArray instanceof iframeArray); // true (but useless to you)
// Even though it quacks exactly like a duck:
console.log(Array.isArray(theirArray)); // true (the correct way to check)
This is absurd, but it’s the reality we live in. For built-ins like Array, Date, or Promise, you should almost always prefer Array.isArray(), typeof, or other checks over instanceof to avoid this cross-realm insanity.
Best Practices and When to Actually Use It
So, given that pitfall, when do you reach for instanceof? The answer is simple: for your own classes. You have complete control over them, and you’re unlikely to be shuttling your own class instances across iframe boundaries in a typical app.
It’s perfect for narrowing in polymorphic code, like event handlers or when dealing with data from a library that returns a base class or an interface.
class ApiSuccess {
constructor(public data: any) {}
}
class ApiError {
constructor(public message: string) {}
}
function handleApiResponse(response: ApiSuccess | ApiError) {
if (response instanceof ApiSuccess) {
// Type is narrowed to ApiSuccess
console.log(`Data received: ${response.data}`);
} else {
// Type is narrowed to ApiError
console.error(`Failed: ${response.message}`);
}
}
This is clean, self-documenting, and type-safe. It’s what instanceof was made for. Just remember its limitations with built-in types and never, ever use it for something as simple as checking if a value is an array. You’re better than that. Use Array.isArray and let TypeScript do the rest.