Skip to content
TypeScript ts narrowing 3 min read

The in Operator

The in operator tests whether a property exists on an object, and TypeScript treats it as a type guard. This is the go-to tool when a union is made of object shapes that differ by which properties they carry — something typeof (only primitives) and instanceof (only classes) cannot handle. Because in is a plain runtime check that survives compilation, it works on interfaces and type aliases that have no constructor. This page covers basic property narrowing, optional properties, the discriminant alternative, and common pitfalls.

Narrowing by property presence

When two object types are distinguished by a unique property, "prop" in value narrows the union to the variant that declares that property.

interface Circle {
  radius: number;
}

interface Square {
  side: number;
}

function area(shape: Circle | Square): number {
  if ("radius" in shape) {
    // shape: Circle
    return Math.PI * shape.radius ** 2;
  }
  // shape: Square
  return shape.side ** 2;
}

After "radius" in shape succeeds, TypeScript knows shape must be the Circle member, so shape.radius is valid. In the else path it narrows to Square.

Why in works where typeof and instanceof do not

Interfaces are erased at compile time, so they have no constructor for instanceof and are not primitives for typeof. The in operator inspects the runtime object directly, making it the right structural guard for interface unions.

GuardBest forSurvives compilation as
typeofprimitive unionsa runtime string check
instanceofclass instancesa prototype-chain check
inobject shapes / interfacesa property-presence check
interface Admin {
  permissions: string[];
}
interface Guest {
  sessionId: string;
}

function describe(user: Admin | Guest): string {
  return "permissions" in user
    ? `admin with ${user.permissions.length} permissions`
    : `guest ${user.sessionId}`;
}

Optional properties and narrowing direction

Be careful with optional members. If a property is declared optional (prop?: T), an in check narrows toward the variant that can have it, but the value may still be undefined at runtime, so guard the value itself when needed.

interface Response {
  data?: string;
  error?: string;
}

function handle(res: Response): string {
  if ("data" in res && res.data !== undefined) {
    return res.data;
  }
  return res.error ?? "no result";
}

The in operator narrows based on declared properties, not actual runtime values. A union member that lists an optional property will be included even when that property is absent at runtime — combine in with a value check for safety.

Discriminated unions are usually clearer

When you control the types, a shared discriminant field is more robust than checking for the existence of distinct properties. It scales to many variants and pairs perfectly with switch.

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; side: number }
  | { kind: "rect"; width: number; height: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.side ** 2;
    case "rect":
      return shape.width * shape.height;
  }
}

Use plain in when the shapes come from external code you cannot reshape; prefer a discriminant when you own the types. See discriminated unions for the full pattern.

Guarding before dynamic access

in is also useful before reading a dynamically named property, narrowing unknown enough to access a key safely.

function hasMessage(value: unknown): value is { message: string } {
  return (
    typeof value === "object" &&
    value !== null &&
    "message" in value &&
    typeof (value as Record<string, unknown>).message === "string"
  );
}

This combines typeof, a null check, and in to build a complete guard for an unknown payload.

Best Practices

  • Use in to narrow unions of object/interface shapes that differ by which keys they carry.
  • Prefer in over instanceof for plain objects and interfaces — they have no runtime constructor.
  • When a distinguishing property is optional, also check that its value is not undefined.
  • Favor a discriminated union with a kind/type field when you control the types.
  • Combine in with typeof and a null check when narrowing unknown payloads.
  • Remember in narrows on declared shape, not runtime values, so guard the value when it matters.
Last updated June 29, 2026
Was this helpful?