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.
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.
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.
const now = new Date();
console.log(typeof now); // "object", not useful
console.log(now instanceof Date); // true, preciseThe 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:
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:
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:
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:
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.
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:
function identify(creature: Dog | Cat) {
if (creature instanceof Dog) {
creature.bark(); // creature is Dog
} else {
creature.meow(); // creature is Cat
}
}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:
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:
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:
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.
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.
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
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.
Frequently Asked Questions
How does TypeScript narrow with instanceof?
Does instanceof work with custom classes?
What is the difference between typeof and instanceof for narrowing?
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.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.