Chapter 6.5โ˜• 14 min read

6.5 โ€” The in Operator Narrowing

in operator checks for distinctive features to narrow types

01The Features Check

The in operator checks if a property EXISTS on an object. At runtime, it returns true or false. But TypeScript takes this runtime check and uses it to narrow union types where each member has DIFFERENT properties. It's the perfect tool for discriminated unions that DON'T share a common discriminant property, or when you simply want to check for a unique feature.

Think of the RTA checkpost here in Hyderabad. The inspector doesn't look at the vehicle's registration card to check the type label. Instead, they do a "features check". They ask: "Does this vehicle have a meter?" If yes, it's an auto. "Does it have a routeDisplay?" If yes, it's an RTC city bus. "Does it have a helmet storage?" If yes, it's a bike. The inspector isn't checking a name tag; they are checking for a DISTINCTIVE FEATURE that only one type of vehicle possesses.

The in operator does exactly this in TypeScript. When you write "meter" in vehicle, TypeScript checks: "Which member of this union has a meter property?" If only the Auto type has it, TypeScript safely narrows the type to Auto. You don't need to add a type: "auto" discriminant if the shape of the object already reveals its identity!

This is incredibly useful when you are consuming types from a third-party library where you can't add a discriminant field, or when working with plain data objects where adding an extra field feels redundant. Seedha samjho: the in operator is like checking for a distinctive feature, not reading the name tag! It's a simple, elegant, and powerful narrowing technique that every TypeScript developer must have in their toolkit.

02in Operator Syntax

The in operator syntax is straightforward: "propName" in obj. It checks if the property name exists on the object (including the prototype chain). TypeScript uses the result to narrow the type of obj within the conditional block. Let's see this with our Hyderabad transport examples:

interface Auto {
  meter: number;
  fare: number;
}
interface Bus {
  routeDisplay: string;
  capacity: number;
}

function getInfo(
  transport: Auto | Bus
) {
  if ("meter" in transport) {
    // transport is narrowed to Auto
    return transport.fare;
  } else {
    // transport is narrowed to Bus
    return transport.capacity;
  }
}

TypeScript sees that only Auto has a meter property. So inside the if block, transport is safely narrowed to Auto, giving you access to transport.fare. In the else block, it must be a Bus, so you get transport.capacity.

What if both types had meter? Then "meter" in transport would be true for BOTH, and TypeScript wouldn't narrow. The property must be UNIQUE to one member for narrowing to work. Here's an example with more union members:

interface Fish { swim(): void; }
interface Bird { fly(): void; }
interface Snake { slither(): void; }

function move(animal: Fish | Bird | Snake) {
  if ("swim" in animal) {
    // animal is Fish
    animal.swim();
  } else if ("fly" in animal) {
    // animal is Bird
    animal.fly();
  } else {
    // animal is Snake
    animal.slither();
  }
}

Each animal has a unique method. The in operator seamlessly narrows the type at each step. Notice how we didn't need any type field or discriminant โ€” the presence of the method itself tells TypeScript exactly which type it is. This makes in incredibly natural for working with plain object shapes.

03in vs Other Techniques

How does in compare to other narrowing techniques in TypeScript? Understanding the differences helps you pick the right tool for the job. Let's break it down:

1. in vs Discriminant: A discriminant uses a shared property with literal values, like transport.type === "auto". Discriminants are more explicit and are the preferred approach when you control the types (see Stage 3.5). Use in when you DON'T control the types and can't add a discriminant, or when the types naturally have unique properties.

2. in vs typeof: typeof obj === "string" only narrows primitive types (string, number, boolean, etc.). "length" in obj can check for ANY property on ANY object. Use typeof for primitives, in for objects.

3. in vs instanceof: obj instanceof MyClass checks the prototype chain and only works with class instances. in works perfectly with plain objects (interfaces, type aliases). Use in when dealing with plain object types, instanceof for actual class instances.

4. in with optional properties: This is subtle. Consider interface A { name: string; email?: string }. Does "email" in a narrow correctly? Yes, TypeScript narrows to A. But be careful โ€” email might be undefined! The property EXISTS, but has no value. We'll cover this trap in detail next.

5. Best practice hierarchy: Prefer discriminants first (most explicit). Then use in for plain objects with unique properties. Finally, use custom type guards for complex logic that can't be expressed with a simple check. The in operator hits the sweet spot between simplicity and power for most object narrowing needs.

04in Operator Traps

The in operator has some tricky edge cases that catch even experienced developers off guard. Let's walk through the most common traps so you don't fall into them at the RTA checkpost!

Trap 1: Existence vs. Value. The in operator checks if a property EXISTS, NOT if it has a truthy value. "name" in obj returns true even if obj.name is undefined, null, false, or 0. Don't use in to check if a value is present โ€” use if (obj.name) or if (obj.name !== undefined) instead.

const obj = { name: undefined };
"name" in obj; // true!
// Key exists, but value is undefined
// Use !== undefined for value check

Trap 2: Prototype Chain. in checks the entire prototype chain. "toString" in {} is TRUE because toString exists on Object.prototype. If you only want to check the object's OWN properties, use obj.hasOwnProperty("prop") โ€” but beware, hasOwnProperty does NOT narrow types in TypeScript!

Trap 3: Optional Property Trap. This is the most dangerous one. If a property is optional, in says it exists, but the value might be undefined.

interface A {
  x: string;
  y?: number;
}
interface B {
  x: string;
  z: boolean;
}

function f(v: A | B) {
  if ("y" in v) {
    // v is A, but v.y might be
    // undefined! Always check:
    if (v.y !== undefined) {
      // NOW v.y is number safely
    }
  }
}

Trap 4: Shared Properties. If two types in the union both have the property, in doesn't narrow at all! You need a UNIQUE property per type for in to work effectively. If interface A and interface B both have shared: number, then "shared" in x is true for both, and no narrowing happens. Always ensure the property you check is distinctive!

05in Operator Cheatsheet

Here's your complete cheatsheet for the in operator โ€” the RTA features check handbook. Pin this to your desk!

Basic Syntax:

"prop" in obj
// Checks if property exists on obj
// Returns true / false

Narrowing Rule:

if ("meter" in transport) {
  // transport is narrowed to the
  // type that HAS a meter property
}

When to Use in:

  • Objects with unique distinguishing properties
  • Plain object types without discriminants
  • Third-party types you can't modify

Alternatives Hierarchy:

// 1. Discriminant (preferred)
if (v.type === "auto") { ... }

// 2. in operator (unique prop)
if ("meter" in v) { ... }

// 3. typeof (primitives only)
if (typeof v === "string") { ... }

// 4. instanceof (class instances)
if (v instanceof Auto) { ... }

Key Rules to Remember:

  • Rule 1: Only narrows if the property is UNIQUE to one union member.
  • Rule 2: Checks existence, not value โ€” undefined still counts as existing.
  • Rule 3: Includes prototype chain properties โ€” "toString" in {} is true.
  • Rule 4: Optional properties still "exist" for in โ€” always follow up with !== undefined checks.
  • Rule 5: Shared properties don't narrow โ€” use unique property names per type.

The golden rule: "The in operator is the RTA features check โ€” look for the distinctive feature, not the name tag. But remember, checking the feature exists is not the same as checking it has a value, bhai!"

Key Points

  • The in operator checks if a property name exists on an object
  • TypeScript narrows the type to the union member that has that property
  • Only narrows if the property is UNIQUE to one union member
  • in checks existence, not value โ€” undefined still means the property exists
  • Includes prototype chain properties (e.g., "toString" in {} is true)
  • Prefer discriminants first, then in, then typeof/instanceof
Course Search
Search across all chapters & stages
๐Ÿ“–

Search the course

Type any topic โ€” branching, stash, rebase, hooks โ€” and jump straight to that chapter.

merge branchesgit stashundo commitrebase