instanceof & in Narrowing

Difficulty: Intermediate

The `instanceof` operator narrows types based on the prototype chain. When you write `x instanceof SomeClass`, TypeScript narrows `x` to `SomeClass` in the true branch. This works with any class, including built-in classes like `Date`, `RegExp`, `Error`, and custom classes. It is the primary way to narrow types when working with class hierarchies.

The `instanceof` check works at runtime by traversing the prototype chain of the object. If the constructor function's `prototype` property appears anywhere in the chain, the check returns true. This means it works with inheritance - an instance of a subclass is also an instance of its parent class. TypeScript's type narrowing respects this relationship.

The `in` operator checks whether a property name exists in an object. When you write `'propertyName' in obj`, TypeScript narrows the type of `obj` to whichever members of a union type contain that property. This is particularly useful for discriminating between object types in a union without needing a dedicated discriminant property.

The `in` operator is a pragmatic alternative to discriminated unions when you are working with types that don't share a common tag property. For example, if you have a union of `Bird | Fish` where Bird has a `fly` method and Fish has a `swim` method, you can use `'fly' in animal` to narrow to Bird. This technique is common when integrating with external APIs or libraries whose types you cannot modify.

Code examples

instanceof Narrowing with Classes

class HttpError {
  constructor(public status: number, public message: string) {}
}

class ValidationError {
  constructor(public field: string, public message: string) {}
}

function handleError(error: HttpError | ValidationError): string {
  if (error instanceof HttpError) {
    return `HTTP ${error.status}: ${error.message}`;
  }
  return `Validation error on '${error.field}': ${error.message}`;
}

console.log(handleError(new HttpError(404, "Not Found")));
console.log(handleError(new ValidationError("email", "Invalid format")));

After the instanceof check, TypeScript knows the error is an HttpError in the if branch and a ValidationError in the else branch, providing access to their specific properties.

in Operator for Property Narrowing

type Bird = { fly: () => string; name: string };
type Fish = { swim: () => string; name: string };

function move(animal: Bird | Fish): string {
  if ("fly" in animal) {
    return animal.fly();
  }
  return animal.swim();
}

const eagle: Bird = { name: "Eagle", fly: () => "Soaring through the sky" };
const salmon: Fish = { name: "Salmon", swim: () => "Swimming upstream" };

console.log(move(eagle));
console.log(move(salmon));

The `in` operator checks for the presence of the `fly` property. TypeScript narrows to Bird in the true branch and Fish in the false branch.

Combining instanceof and in

class ApiResponse {
  constructor(public data: unknown) {}
}

function processResponse(response: ApiResponse): string {
  const data = response.data;

  if (data instanceof Array) {
    return `Array with ${data.length} items`;
  }

  if (typeof data === "object" && data !== null && "message" in data) {
    return `Message: ${(data as { message: string }).message}`;
  }

  return `Raw: ${String(data)}`;
}

console.log(processResponse(new ApiResponse([1, 2, 3])));
console.log(processResponse(new ApiResponse({ message: "Success" })));
console.log(processResponse(new ApiResponse("text")));

This example combines instanceof (for Array) and in (for checking object shape) to safely narrow unknown data into specific types.

Key points

Concepts covered

instanceof operator, in operator, class narrowing, property checking