TypeScriptinstanceof & in Operators

instanceof & in Operators

TypeScript's type narrowing is one of its killer features: the compiler analyses your control flow and automatically refines types inside branches. Two of the most powerful narrowing tools are the instanceof and in operators — both from JavaScript, but deeply understood by the TypeScript type checker.

The instanceof Operator

instanceof tests whether an object was created by a particular constructor. At runtime it walks the prototype chain; TypeScript uses it to narrow the type inside the branch.

TS
class Dog {
  bark() { return 'Woof!' }
}

class Cat {
  meow() { return 'Meow!' }
}

function makeNoise(animal: Dog | Cat) {
  if (animal instanceof Dog) {
    // TypeScript knows: animal is Dog here
    console.log(animal.bark())
  } else {
    // TypeScript knows: animal is Cat here
    console.log(animal.meow())
  }
}
Note
The narrowed type inside the instanceof branch is the constructor's instance type, not the original union. TypeScript removes the other branches from consideration.
instanceof with Class Hierarchies

instanceof works correctly with inheritance. TypeScript narrows to the most specific matching class in the hierarchy.

TS
class Animal {
  breathe() { return 'breathing' }
}

class Dog extends Animal {
  bark() { return 'Woof!' }
}

class GoldenRetriever extends Dog {
  fetch() { return 'fetching!' }
}

function handle(a: Animal) {
  if (a instanceof GoldenRetriever) {
    console.log(a.fetch()) // GoldenRetriever — has .fetch(), .bark(), .breathe()
  } else if (a instanceof Dog) {
    console.log(a.bark())  // Dog — has .bark(), .breathe()
  } else {
    console.log(a.breathe()) // just Animal
  }
}
Tip
Order matters: always check the most specific subclass first. If you checkinstanceof Dog before instanceof GoldenRetriever, the GoldenRetriever branch becomes dead code because GoldenRetriever instances also satisfy instanceof Dog.
The in Operator

The in operator tests whether a property exists on an object: 'prop' in obj. TypeScript uses it to narrow union types based on which members have a given property. This is invaluable when working with plain objects (no class hierarchy).

TS
interface Circle {
  kind: 'circle'
  radius: number
}

interface Rectangle {
  kind: 'rectangle'
  width: number
  height: number
}

type Shape = Circle | Rectangle

function area(shape: Shape): number {
  if ('radius' in shape) {
    // TypeScript narrows to Circle
    return Math.PI * shape.radius ** 2
  } else {
    // TypeScript narrows to Rectangle
    return shape.width * shape.height
  }
}
in with Optional Properties

in is especially useful when properties are optional. If one branch of a union has an optional property and another doesn't have it at all, in cleanly separates them.

TS
interface AdminUser {
  name: string
  role: 'admin'
  permissions: string[]
}

interface GuestUser {
  name: string
  role: 'guest'
}

type User = AdminUser | GuestUser

function describeUser(user: User) {
  if ('permissions' in user) {
    // Narrowed to AdminUser
    console.log(`Admin with ${user.permissions.length} permissions`)
  } else {
    // Narrowed to GuestUser
    console.log('Guest user — limited access')
  }
}

describeUser({ name: 'Alice', role: 'admin', permissions: ['read', 'write'] })
describeUser({ name: 'Bob', role: 'guest' })
Admin with 2 permissions
Guest user — limited access
Combining instanceof and in

Real-world code often mixes class instances and plain objects in the same union. You can combine both operators freely.

TS
class ApiError {
  constructor(
    public statusCode: number,
    public message: string,
  ) {}
}

interface NetworkTimeout {
  type: 'timeout'
  durationMs: number
}

type Failure = ApiError | NetworkTimeout | Error

function handleFailure(failure: Failure) {
  if (failure instanceof ApiError) {
    console.log(`API Error ${failure.statusCode}: ${failure.message}`)
  } else if ('durationMs' in failure) {
    // Narrowed to NetworkTimeout
    console.log(`Timed out after ${failure.durationMs}ms`)
  } else {
    // What's left: Error
    console.log(`Unexpected error: ${failure.message}`)
  }
}
Common Mistakes

Mistake

Problem

Fix

instanceof on plain objects

Plain objects have no constructor — always false

Use the in operator instead

in on null/undefined

Runtime TypeError: cannot use in with null

Guard with != null first

Most-specific class check last

Subclass instances caught by parent check first

Always check narrowest type first

in with number literals

in checks property names (strings/symbols)

Use string keys: "0" in arr

Safe null handling with in

TS
// instanceof handles null safely (returns false)
function checkDate(val: Date | null) {
  if (val instanceof Date) {
    return val.toISOString()
  }
  return 'no date'
}

// in does NOT handle null — throws TypeError at runtime!
function safePropCheck(val: { name: string } | null) {
  // WRONG: if ('name' in val) — crashes when val is null
  if (val !== null && 'name' in val) { // correct guard
    return val.name
  }
  return 'anonymous'
}
Warning
The in operator throws a TypeError at runtime if the right-hand side isnull or undefined. Always guard with a null check first.instanceof is safer — it returns false for null.
Real-World: Error Handling Pattern

TS
// A robust error handler for a fetch wrapper
class HttpError extends Error {
  constructor(
    public status: number,
    message: string,
  ) {
    super(message)
    this.name = 'HttpError'
  }
}

class ValidationError extends Error {
  constructor(
    public field: string,
    message: string,
  ) {
    super(message)
    this.name = 'ValidationError'
  }
}

function handleError(err: unknown): string {
  if (err instanceof HttpError) {
    return `HTTP ${err.status}: ${err.message}`
  }

  if (err instanceof ValidationError) {
    return `Validation failed on "${err.field}": ${err.message}`
  }

  if (err instanceof Error) {
    return `Error: ${err.message}`
  }

  // err is unknown — could be a string throw
  return `Unknown failure: ${String(err)}`
}
Success
Using instanceof in error handlers gives you full type safety when catching from try/catch (where the caught value is typed as unknown). This is far safer than casting with as.
Quick Reference

Operator

Best for

Works with

instanceof

Class instances, Error subclasses

Classes (have constructors)

in

Plain objects, interface discrimination

Any object with properties

typeof

Primitives (string, number, boolean)

Primitive values

Discriminant field

Tagged unions, API responses

Objects with a shared literal field