Type Narrowing with instanceof in TypeScript

TypeScript uses the instanceof operator to narrow union types containing classes. Learn how it works with built-in types like Date and Error, custom classes, and the prototype chain.

6 min read

The instanceof operator checks whether an object's prototype chain contains a specific constructor. TypeScript treats it as a type guard: checking if a value is an instance of Date narrows it to Date in the true branch and removes Date from the union in the false branch.

typescripttypescript
function logValue(x: Date | string) {
  if (x instanceof Date) {
    console.log(x.toUTCString()); // x is Date
  } else {
    console.log(x.toUpperCase()); // x is string
  }
}
 
logValue(new Date());         // "Thu, 17 Jul 2026 ..."
logValue("hello");            // "HELLO"

The instanceof check splits the union the same way typeof does, but it works on class instances instead of primitives.

Why instanceof Exists Alongside typeof

The typeof operator only handles primitives, and it cannot tell class instances apart from plain objects, so instanceof fills that gap for anything created with the new keyword.

typescripttypescript
const now = new Date();
console.log(typeof now);          // "object", not useful
console.log(now instanceof Date); // true, precise

The typeof result is the same generic "object" string for every non-function object. The instanceof check tells you exactly which class you actually have.

Built-in Classes: Date, Error, Map, Set

The instanceof operator works with every built-in JavaScript class, not just custom ones. A function that accepts several class types can check each one in turn:

typescripttypescript
function handle(value: Date | Error | string) {
  if (value instanceof Date) {
    console.log("Date:", value.toISOString());
  } else if (value instanceof Error) {
    console.log("Error:", value.message);
  } else {
    console.log("String:", value.toUpperCase());
  }
}

TypeScript narrows correctly through each check in order. After the Date and Error branches are handled, only the string case remains in the final else. Collections work the same way:

typescripttypescript
function processCollection(col: Map<string, number> | Set<string>) {
  if (col instanceof Map) {
    col.forEach((value, key) => console.log(key, value)); // col is Map
  } else {
    col.forEach(value => console.log(value));              // col is Set
  }
}

Custom Classes

The same technique works for your own classes, not just built-in ones. Two custom error classes can each carry different extra fields:

typescripttypescript
class ApiError extends Error {
  constructor(public statusCode: number, message: string) {
    super(message);
  }
}
class ValidationError extends Error {
  constructor(public fields: string[], message: string) {
    super(message);
  }
}

Narrowing with instanceof unlocks the field that belongs to each specific error class, the same way narrowing a plain union unlocks a member-only property or method:

typescripttypescript
function handleError(err: ApiError | ValidationError) {
  if (err instanceof ApiError) {
    console.log(`API error ${err.statusCode}: ${err.message}`);
  } else {
    console.log(`Validation error on: ${err.fields.join(", ")}`);
  }
}

The statusCode field only exists on the API error class, and fields only exists on the validation error class. TypeScript enforces this after narrowing, so accessing the wrong field in the wrong branch is a compile error.

Subclass Narrowing and the Prototype Chain

The instanceof operator follows the whole prototype chain, not just the exact class. An instance of a subclass also passes an instanceof check against its parent class.

typescripttypescript
class Animal {
  constructor(public name: string) {}
}
class Dog extends Animal {
  bark() { console.log("Woof!"); }
}
class Cat extends Animal {
  meow() { console.log("Meow!"); }
}

Checking against the more specific subclass narrows to that subclass, unlocking methods that only exist there and are not part of the shared parent class:

typescripttypescript
function identify(creature: Dog | Cat) {
  if (creature instanceof Dog) {
    creature.bark(); // creature is Dog
  } else {
    creature.meow(); // creature is Cat
  }
}
Prototype chain and instanceof checks

A Dog instance passes both an instanceof check against Dog and one against Animal, because Dog sits below Animal on the prototype chain. TypeScript narrows to whichever class you actually checked against, not the most specific possible type.

Narrowing with else if Chains

Stack multiple instanceof checks the same way you stack typeof checks, handling the most specific class before falling through to a generic case:

typescripttypescript
function parse(input: string | Date | Error) {
  if (input instanceof Date) {
    return input.toISOString();
  } else if (input instanceof Error) {
    return input.message;
  }
  return input.trim();
}

Check order matters here. A parent-class check placed first would also match every subclass instance, so put more specific checks before more general ones:

typescripttypescript
class AppError extends Error {}
 
function handle(err: AppError | Error) {
  if (err instanceof AppError) {
    console.log("App error:", err.message);
  } else {
    console.log("Generic error:", err.message);
  }
}

Reversing the order would break this. An instanceof check against the plain Error class placed first would catch both the subclass and plain Error instances, so the more specific branch would never run.

Combining typeof and instanceof

Real functions often mix primitives and class instances in the same union. Use typeof for the primitive members and instanceof for the class members:

typescripttypescript
function serialize(value: string | number | Date | null) {
  if (value === null) return "null";
  if (value instanceof Date) return value.toISOString();
  if (typeof value === "number") return value.toString();
  return value; // string
}

The order of checks follows a natural priority: handle null first, then instanceof, then typeof. TypeScript tracks the narrowing correctly through each step, leaving only the string case for the final return.

Common Mistakes

Using instanceof on primitives. The instanceof operator only works with objects, not primitives, so it misses ordinary string values entirely.

typescripttypescript
function bad(value: string | Date) {
  if (value instanceof String) {
    console.log("This branch almost never runs");
  }
}

Use a typeof check for primitive strings instead. Comparing against the boxed String class only matches values built with new String(), which real code should rarely use.

Assuming instanceof narrows to the most specific type. It narrows to the type you actually checked against. Checking against a parent class narrows to that parent, not to whatever subclass the value happens to be.

Using instanceof across execution contexts. The prototype chain check can fail across iframes or different JavaScript realms, since each realm has its own separate class constructors. For cross-realm checks, use duck typing or a custom discriminant property instead.

Forgetting that instanceof runs at runtime. The check itself is not erased by the compiler. Only the type narrowing is compile-time behavior. The comparison expression still executes in JavaScript every time the code runs.

When to Prefer a Discriminant Over instanceof

For complex state modeling, a discriminated union is often clearer than a chain of instanceof checks. A discriminant works with plain objects and does not depend on the prototype chain at all.

typescripttypescript
type ApiFailure =
  | { type: "api"; statusCode: number; message: string }
  | { type: "validation"; fields: string[]; message: string };
 
function report(error: ApiFailure) {
  if (error.type === "api") {
    console.log(`API error ${error.statusCode}: ${error.message}`);
  } else {
    console.log(`Validation error on: ${error.fields.join(", ")}`);
  }
}

This reads the same way the earlier custom error classes did, but narrowing here depends only on the type field, not on a class hierarchy. Use instanceof when you already have class instances, such as errors thrown by a library. Use a discriminant when you control the data shape from the start.

For narrowing primitive types, see type narrowing with typeof. For narrowing by property existence on plain objects, use the in operator type guard.

Rune AI

Rune AI

Key Insights

  • instanceof checks inside if statements act as type guards for class-based types.
  • Use instanceof for Date, Error, Map, Set, custom classes, and any value created with new.
  • instanceof follows the prototype chain: a subclass instance passes checks against parent classes.
  • Combine instanceof with typeof: typeof for primitives, instanceof for class instances.
  • instanceof narrowing is compile-time only; the check itself runs at runtime.
RunePowered by Rune AI

Frequently Asked Questions

How does TypeScript narrow with instanceof?

When you write if (x instanceof Date), TypeScript treats it as a type guard. In the true branch, x is narrowed to Date. In the false branch, Date is removed from the union. This works for any class.

Does instanceof work with custom classes?

Yes. TypeScript narrows correctly through any class in the prototype chain. If you have class Dog extends Animal, then dog instanceof Animal is true and TypeScript narrows to Animal.

What is the difference between typeof and instanceof for narrowing?

typeof narrows primitive types (string, number, boolean, etc.). instanceof narrows class instances. typeof checks the JavaScript type tag; instanceof checks the prototype chain.

Conclusion

instanceof narrowing handles every class-based type that typeof cannot. Use it with built-in classes like Date and Error, with your own custom classes, and with subclass hierarchies. TypeScript automatically follows the prototype chain, so a check against a parent class narrows to that parent.