Learn JS Series (#42) - instanceof, isPrototypeOf, and Checking Types Honestly

avatar

Learn JS Series (#42) - instanceof, isPrototypeOf, and Checking Types Honestly

js-banner.png

What will I learn

  • You will learn how instanceof actually works: it walks the prototype chain;
  • how isPrototypeOf asks the same question more directly;
  • why typeof and instanceof answer different questions, and when to use each;
  • the reliable ways to check for arrays, null, and specific object kinds;
  • the pitfalls of instanceof (subclasses, cross-realm objects) and safer alternatives.

Requirements

  • A working modern computer running macOS, Windows or Ubuntu;
  • An installed Node.js (20+) distribution, or just a modern browser console;
  • Episodes 1-41 read, especially the prototype chain and classes.

Difficulty

  • Intermediate

Curriculum (of the Learn JS Series):

Learn JS Series (#42) - instanceof, isPrototypeOf, and Checking Types Honestly

Last episode we locked data DOWN with # private fields, and right at the end I dropped a little teaser: the #field in obj brand check was, in some ways, a more honest way to ask "is this a real instance of my class?" than the operator most people reach for. That operator is instanceof, and today it gets its day in court. Here is the thing about type checking in JavaScript: almost everyone uses instanceof and typeof every day, and almost nobody can tell you what they actually DO under the hood. They treat them as magic incantations -- "this one for objects, that one for primitives" -- and mostly get away with it, right up until the day one of them lies to their face and they have no idea why. Well, you are not going to be that person. You already understand the prototype chain (episodes 34-36), and once you see that instanceof is nothing more than a walk along that chain, every result it gives you becomes obvious in stead of mysterious. Let's have a proper look. ;-)

Solutions to Episode 41 Exercises

Exercise 1 - a locked-down account:

class Account {
  #balance = 0;
  deposit(n) { if (n < 0) throw new Error("no negatives"); this.#balance += n; return this.#balance; }
  get balance() { return this.#balance; }
}
const a = new Account();
a.deposit(100);
console.log(a.balance); // 100
// a.#balance; // SyntaxError - no outside access at all

The insight: #balance has no back door; the getter is the only read path and deposit is the only write path, so the validation in deposit can never be skipped.

Exercise 2 - a password box:

class PasswordBox {
  #value = "";
  set(value) { this.#value = value; }
  matches(guess) { return guess === this.#value; }
}
const box = new PasswordBox();
box.set("hunter2");
console.log(box.matches("hunter2"), box.matches("wrong")); // true false

The insight: the value is never exposed, only a boolean comparison ever leaves the object, so there is genuinely no way to read the stored password from outside.

Exercise 3 - unique ids via private static:

class Item {
  static #next = 1;
  #id;
  constructor() { this.#id = Item.#next++; }
  get id() { return this.#id; }
}
console.log(new Item().id, new Item().id, new Item().id); // 1 2 3

The insight: # privacy is enforced by the language, unlike the _underscore convention which anyone could ignore and quietly corrupt.

Now let's ask the question "what kind of thing is this object?", honestly and correctly.

instanceof walks the prototype chain

The instanceof operator answers: "is this object an instance of that class (or constructor)?". But under the hood it is not checking some stored type tag hidden on the object. It does something you now fully understand: it checks whether the constructor's prototype object appears anywhere in the object's prototype chain (episode 35). That is the entire mechanism, top to bottom:

class Animal {}
class Dog extends Animal {}

const rex = new Dog();
console.log(rex instanceof Dog);    // true - Dog.prototype is in rex's chain
console.log(rex instanceof Animal); // true - Animal.prototype is too (via extends)
console.log(rex instanceof Object); // true - Object.prototype is at the top
console.log(rex instanceof Array);  // false - Array.prototype is NOT in the chain

rex instanceof Animal is true because extends put Animal.prototype on rex's chain (rex -> Dog.prototype -> Animal.prototype -> Object.prototype -> null). So instanceof naturally respects inheritance: a Dog is an Animal and an Object, because all three prototypes sit on its chain. Knowing it is a chain walk, not a type label, explains every single result it will ever give you. If you can point to where a prototype lives in the chain, you can predict the answer before you run the code.

We can even prove the equivalence by hand. Here is instanceof written out as a plain function, walking the chain link by link exactly the way the operator does internally:

function myInstanceof(obj, Ctor) {
  let proto = Object.getPrototypeOf(obj);
  while (proto !== null) {
    if (proto === Ctor.prototype) return true; // found it in the chain
    proto = Object.getPrototypeOf(proto);       // step one link up
  }
  return false; // reached the end (null) without a match
}
console.log(myInstanceof(rex, Animal)); // true - same answer as the real operator
console.log(myInstanceof(rex, Array));  // false

That loop IS instanceof, more or less. It grabs the object's prototype, compares it against Ctor.prototype, and keeps climbing until it either finds a match or falls off the top of the chain at null. Once you have seen it spelled out like this, the operator stops being magic. It is a while loop you could have written yourself.

Customizing instanceof with Symbol.hasInstance

Here is a lovely detail that surprises people: instanceof is not entirely hard-wired. When you write x instanceof C, the engine first looks for a method C[Symbol.hasInstance] and, if it exists, calls it to decide the answer. The default lives on Function.prototype and does the chain walk we just saw, but you can override it to make instanceof mean whatever you like:

class Even {
  static [Symbol.hasInstance](value) {
    return Number.isInteger(value) && value % 2 === 0;
  }
}
console.log(4 instanceof Even);   // true - 4 is even
console.log(7 instanceof Even);   // false
console.log("x" instanceof Even); // false

Notice we never construct an Even, and the operands are plain numbers, not objects. By defining Symbol.hasInstance, we hijacked instanceof into a custom predicate. This is rarely something you should DO in everyday code (surprising your reader is expensive), but it is worth knowing, because it explains WHY instanceof is a little more flexible than a fixed chain-walk, and it is exactly the kind of hook libraries use to make their objects answer instanceof correctly across tricky boundaries.

isPrototypeOf: the same question, directly

instanceof is really a convenience wrapper around a more direct question. proto.isPrototypeOf(obj) asks: "is proto somewhere in obj's prototype chain?", which is exactly what instanceof checks, just phrased in terms of the prototype object rather than the constructor:

class Animal {}
class Dog extends Animal {}
const rex = new Dog();

console.log(Dog.prototype.isPrototypeOf(rex));    // true - same as rex instanceof Dog
console.log(Animal.prototype.isPrototypeOf(rex)); // true

// works for plain Object.create chains too, where there is no constructor:
const base = { kind: "base" };
const derived = Object.create(base);
console.log(base.isPrototypeOf(derived)); // true - base is in derived's chain

isPrototypeOf is especially handy for the Object.create style (episodes 34-36), where there may be no constructor function to feed to instanceof, you just have bare prototype objects linked together. It is the lower-level primitive; instanceof is the friendlier syntax sitting on top of the same idea. If you ever find yourself thinking "I have the prototype object but not a constructor", isPrototypeOf is the tool you want.

typeof versus instanceof: different questions

Beginners often reach for the wrong one, and then wonder why the answer is useless. typeof and instanceof answer different questions, and knowing which is which is half the battle:

  • typeof returns a string naming the primitive type (episode 3): "number", "string", "boolean", "function", "object", "undefined", "symbol", "bigint". Use it to distinguish primitives, and to check for "function" and "undefined".
  • instanceof checks whether an object descends from a particular class or prototype. Use it to check what kind of object something is.
console.log(typeof 42);            // "number" - a primitive kind
console.log(typeof "hi");          // "string"
console.log(typeof function(){});  // "function"
console.log(typeof undefined);     // "undefined"

console.log([] instanceof Array);        // true - what kind of object
console.log(new Date() instanceof Date); // true
console.log(typeof []);                  // "object" - typeof cannot tell arrays from objects

Notice typeof [] is just "object", useless for distinguishing arrays from plain objects; telling them apart is instanceof's (or Array.isArray's) job. And 42 instanceof Number is false, because the primitive 42 is not an object at all -- instanceof only ever makes sense on the right-hand side of a new. So the rule of thumb writes itself: reach for typeof on primitives, instanceof on objects. Use the wrong tool and you get a technically-true-but-worthless answer like "object" for every non-primitive under the sun.

The reliable checks for common cases

A few specific type checks deserve their own dedicated tools, because the general operators actively mislead you here. These are the ones I want burned into your memory, because you will use them constantly:

For arrays, do not use instanceof Array (it has a cross-context flaw we will get to below); use the purpose-built Array.isArray:

console.log(Array.isArray([1, 2, 3])); // true - the reliable array check
console.log(Array.isArray("abc"));      // false
console.log(Array.isArray({ length: 3 })); // false - an array-like is not an array

For null, remember typeof null is the famously buggy "object" (episode 3), a wart that has been in the language since 1995 and can never be fixed without breaking the web. So check for null directly, and combine both facts to test for "a real object":

const value = null;
console.log(value === null);                               // the correct null check
console.log(typeof value === "object" && value !== null);  // "is a real non-null object"

For a precise built-in tag, the classic Object.prototype.toString.call(x) reveals the object's internal [[Class]]-style type name, which is the most reliable way to tell built-in kinds apart:

const tag = (x) => Object.prototype.toString.call(x);
console.log(tag([]));         // "[object Array]"
console.log(tag(new Date())); // "[object Date]"
console.log(tag(null));       // "[object Null]"
console.log(tag(/x/));        // "[object RegExp]"
console.log(tag(() => {}));   // "[object Function]"

This borrowed-method trick (we used call to borrow a method back in episode 23) gives a reliable internal tag when you genuinely need to distinguish a Date from a RegExp from a plain object. Frontends and libraries lean on it precisely because it survives some of the traps that catch instanceof.

We can wrap all of this good sense into one small helper that gives back a single, honest string for any value you throw at it:

function typeOf(value) {
  if (value === null) return "null";        // fix typeof null
  if (Array.isArray(value)) return "array"; // fix typeof [] === "object"
  return typeof value;                       // trust typeof for everything else
}
console.log(typeOf([]));        // "array"
console.log(typeOf(null));      // "null"
console.log(typeOf(42));        // "number"
console.log(typeOf({}));        // "object"
console.log(typeOf(() => {}));  // "function"

That tiny function patches the two spots where typeof embarrasses itself (null and arrays) and defers to it everywhere it is already correct. I keep something like this in most codebases, because "what is this, really?" comes up more often than you would think.

The pitfalls of instanceof

instanceof is genuinely useful, but it has two real gotchas that bite people, and both are worth understanding rather than just memorizing. First, it returns true for every ancestor. That is usually what you want, but it means instanceof cannot tell you the exact class, only that the object descends from one somewhere. A Dog is instanceof Animal, so an instanceof Animal test cannot distinguish a Dog from a Cat. When you truly need the precise class, compare the constructor or the immediate prototype directly:

class Animal {}
class Dog extends Animal {}
const rex = new Dog();
console.log(rex instanceof Animal);                        // true - but so is a Cat
console.log(Object.getPrototypeOf(rex) === Dog.prototype); // true - EXACTLY a Dog
console.log(rex.constructor === Dog);                      // true - exact class check

Second, the famous cross-realm problem. Because instanceof compares against the identity of a specific prototype object, an array created in a different JavaScript context has a different Array.prototype, and so arr instanceof Array can return false even though the thing is genuinely, undeniably an array. A "realm" here means a separate global environment: a browser iframe, a <script> in another window, a Node vm context, or a worker. Each gets its own fresh set of built-ins:

// Conceptually, in a browser:
// const iframeArray = window.frames[0].Array;
// const arr = new iframeArray(1, 2, 3);
// arr instanceof Array   -> false! different Array.prototype per realm
// Array.isArray(arr)     -> true.  isArray is realm-safe by design
console.log(Array.isArray([1, 2, 3])); // true - the check that always works

This cross-realm trap is exactly why Array.isArray exists in the first place: it does not compare prototypes, it inspects the internal kind, so it is right no matter which realm made the array. The general lesson: prefer purpose-built checks (Array.isArray, the #field in obj brand check from episode 41, or structural "duck typing") over instanceof whenever your values might cross a context boundary. Inside a single, self-contained module instanceof is perfectly fine; the moment iframes, workers, or multiple bundles enter the picture, treat it with suspicion.

Where instanceof genuinely shines: custom errors

Lest this all read as a hit-piece on instanceof, let me show you the case where it is precisely the right tool: distinguishing error types in a catch block. When you define custom error classes and throw them, instanceof lets a single catch sort out what went wrong and respond differently to each kind:

class ValidationError extends Error {}
class NetworkError extends Error {}

function handle(err) {
  if (err instanceof ValidationError) return `fix your input: ${err.message}`;
  if (err instanceof NetworkError)    return `retrying: ${err.message}`;
  throw err; // something we did not anticipate - do not swallow it
}
console.log(handle(new ValidationError("email missing"))); // "fix your input: email missing"
console.log(handle(new NetworkError("timeout")));          // "retrying: timeout"

Here you WANT the inheritance-aware behaviour: a ValidationError is also an Error, and asking instanceof ValidationError cleanly separates the failure modes so each gets its own recovery path. This is idiomatic, readable, and exactly what instanceof was designed for. (One small caveat carried over from the realm discussion: if errors can cross module or realm boundaries, some codebases add a name check as a belt-and-braces backup. In a single application, the plain instanceof above is spot on.)

A quick look sideways: type checking in Python and Rust

Quite some of you came to this series from the Learn Python Series, so a short comparison sharpens what JavaScript is doing by showing you the neighbours. Python's isinstance(obj, Cls) is the direct cousin of instanceof: it too is inheritance-aware and returns True for any ancestor class. Python also has type(obj) is Cls for an EXACT class check -- the precise mirror of our obj.constructor === Dog line above:

class Animal: pass
class Dog(Animal): pass
rex = Dog()
print(isinstance(rex, Animal))   # True  - like instanceof (any ancestor)
print(type(rex) is Dog)          # True  - exact class, like constructor === Dog
print(type(rex) is Animal)       # False - not the exact class

The parallel is almost one-to-one: isinstance is instanceof, type(x) is C is x.constructor === C. Rust, being statically typed, mostly settles these questions at compile time -- the type of a value is known before the program runs, so there is far less need to interrogate it at runtime, and pattern matching (match) handles the "which variant is this?" cases that JavaScript solves with instanceof on error classes. The takeaway is that "what kind of value is this?" is a universal question every language answers, and JavaScript's dynamic, prototype-based answer is just one point on a spectrum from Python's runtime isinstance to Rust's compile-time guarantees.

Duck typing: an alternative mindset

There is a whole different philosophy worth putting on the table: duck typing -- "if it walks like a duck and quacks like a duck, treat it as a duck". Instead of asking "is this an instance of class X?", you ask "does this object have the capabilities I actually need?". You check for the methods or properties you are going to use, and you do not care one bit about the object's lineage:

function process(thing) {
  // don't ask "is it a Stream?"; ask "can it do what I need?"
  if (typeof thing.read === "function") {
    return thing.read();
  }
  throw new Error("expected something readable");
}

This structural, capability-based checking is often more flexible and more in keeping with JavaScript's dynamic nature than rigid instanceof checks. It does not care whether thing inherited read from a base class, got it from a mixin (next episode), or just happens to have it as an own method -- it only cares that the method is there. Both approaches earn their keep: instanceof when you genuinely need to know the type or lineage (like the custom-error dispatch above), duck typing when you only care about what the object can DO. Choosing well between the two is a real part of writing idiomatic JavaScript, and honestly it is one of those judgement calls that quietly separates comfortable JS developers from struggling ones.

Try it yourself

  1. Build a class hierarchy Vehicle -> Car -> SportsCar with extends, create a SportsCar instance, and check it with instanceof against all three classes plus Object and Array. Explain each result in one sentence in terms of the prototype chain.
  2. Write a function describeType(value) that returns "array", "null", "object", or the plain typeof result, using Array.isArray, an explicit === null check, and typeof. Test it with an array, null, a plain object, a number, and a function.
  3. Show that instanceof Animal cannot distinguish a Dog from a Cat (both extend Animal), then use constructor or Object.getPrototypeOf to check for the exact class. Finish by explaining in one sentence why Array.isArray is safer than instanceof Array.

So what did we actually cover?

  • instanceof checks whether a constructor's prototype appears anywhere in the object's prototype chain -- it is literally a while loop climbing that chain, which is why it naturally respects inheritance.
  • instanceof can be customized with Symbol.hasInstance; isPrototypeOf asks the same chain question directly in terms of the prototype object, handy for Object.create chains that have no constructor.
  • typeof names primitive types (and "function"); instanceof identifies object kinds. Use typeof on primitives, instanceof on objects, and do not swap them.
  • Use dedicated checks for the tricky cases: Array.isArray for arrays, === null for null, and Object.prototype.toString.call(x) for a precise built-in tag -- a small typeOf helper wraps them all up nicely.
  • instanceof cannot give the exact class (every ancestor matches) and can lie across realms; use constructor/prototype comparison for exactness and Array.isArray/brand checks for robustness. It shines for custom-error dispatch in a catch.
  • Duck typing (checking for the capabilities you need rather than the lineage) is often the more flexible, idiomatic alternative when you only care what an object can do.

Next episode we look at sharing behaviour without a single inheritance line: a composition technique for blending capabilities into classes and objects, which will lean directly on the capability-first thinking we just met in duck typing.

Thanks for reading -- catch you in the next one.

@scipio



0
0
0.000
0 comments