Use the in Operator for Type Narrowing

JavaScript's in operator checks if a property exists on an object. TypeScript uses it as a type guard to narrow union types by property presence. Learn how it works and when to use it.

6 min read

JavaScript's in operator checks whether an object has a property with a given name. TypeScript treats "prop" in value as a type guard: the true branch keeps only the union members that have that property, and the false branch keeps only the members that do not.

typescripttypescript
type Fish = { swim: () => void };
type Bird = { fly: () => void };
 
function move(animal: Fish | Bird) {
  if ("swim" in animal) {
    animal.swim(); // animal is Fish
  } else {
    animal.fly();  // animal is Bird
  }
}

TypeScript sees "swim" in animal and narrows animal to Fish in the true branch because Fish has swim and Bird does not. In the false branch, only Bird remains.

Why in Narrowing Exists

typeof distinguishes primitives, and instanceof distinguishes class instances, but neither helps when a union holds plain object types with different property sets. Both Cat and Dog below are ordinary objects, so typeof pet would return "object" for either one, and neither is a class instance for instanceof to check.

typescripttypescript
type Cat = { meow: () => void };
type Dog = { bark: () => void };
 
function speak(pet: Cat | Dog) {
  if ("meow" in pet) {
    pet.meow();
  } else {
    pet.bark();
  }
}

The in operator fills the gap between typeof for primitives and instanceof for classes. It handles plain object unions by checking for property existence instead.

Narrowing strategy: choosing the right operator

How in Narrowing Works

TypeScript looks at every member of the union. Members that have the checked property stay in the true branch. Members that do not have it stay in the false branch.

typescripttypescript
type Car = { drive: () => void; wheels: number };
type Boat = { sail: () => void; length: number };
type Plane = { fly: () => void; altitude: number };
 
type Vehicle = Car | Boat | Plane;

Chaining in checks with else if peels off one member at a time, the same way chained typeof checks do for primitives:

typescripttypescript
function operate(v: Vehicle) {
  if ("drive" in v) {
    console.log(v.wheels); // v is Car
  } else if ("sail" in v) {
    console.log(v.length); // v is Boat
  } else {
    console.log(v.altitude); // v is Plane
  }
}

Each check removes one possibility, so by the final else, TypeScript knows only Plane is left.

Optional Properties and the in Operator

An optional property is tricky: it might exist or might not. TypeScript keeps types with optional properties in both branches of an in check.

typescripttypescript
type Fish = { swim: () => void };
type Bird = { fly: () => void };
type Duck = { swim?: () => void; fly?: () => void };
 
function move(animal: Fish | Bird | Duck) {
  if ("swim" in animal) {
    // animal is Fish | Duck (Duck has swim as optional)
  } else {
    // animal is Bird | Duck (Duck's swim might be absent)
  }
}

Duck has swim as an optional property, so it appears in both branches. TypeScript cannot assume swim is missing on a Duck just because the in check returned false.

Narrowing with in on Nested Properties

The in operator only checks direct properties, not nested ones. To narrow on nested shapes, use a discriminated union or a custom type guard instead.

typescripttypescript
type Success = { data: { items: string[] } };
type Failure = { error: { message: string } };
 
function handle(result: Success | Failure) {
  if ("data" in result) {
    console.log(result.data.items.length); // result is Success
  } else {
    console.log(result.error.message);     // result is Failure
  }
}

The data property directly exists on Success but not on Failure, so the check works. If data were nested inside another object instead of at the top level, in would not detect it.

Combining in with Other Guards

Real functions often need more than one narrowing step. A result union that mixes an error case with two different success value types needs both in and typeof:

typescripttypescript
type StringResult = { value: string };
type NumberResult = { value: number };
type ErrorResult = { error: string };
 
type Result = StringResult | NumberResult | ErrorResult;

The in check removes the error case first, then a typeof check splits the two remaining success shapes, the same layered approach used throughout this article:

typescripttypescript
function display(r: Result) {
  if ("error" in r) {
    console.log("Failed:", r.error);
    return;
  }
  if (typeof r.value === "string") {
    console.log(r.value.toUpperCase());
  } else {
    console.log(r.value.toFixed(2));
  }
}

By the time the function reaches the typeof check, r is already narrowed to StringResult | NumberResult, since the early return handled ErrorResult.

in vs Discriminated Unions

An in check and a discriminated union solve the same problem from different angles:

ApproachHow it worksBest for
in operatorCheck which properties existAd-hoc unions, external data
Discriminated unionCheck a shared kind fieldTypes you control, exhaustive switches

If you own the type definitions, a discriminated union gives you exhaustiveness checking and clearer intent. If you receive data with unpredictable shapes, in is more flexible.

Common Mistakes

Checking a property that exists on every member. If all union members have the property, in does not narrow anything because every type stays in both branches.

typescripttypescript
type A = { id: string; name: string };
type B = { id: string; age: number };
 
function process(x: A | B) {
  if ("id" in x) {
    console.log(x.id); // x is still A | B, both have id
  }
}

Using in on primitives. The in operator only works on objects. Checking "length" in "hello" is a runtime error. Always narrow primitives out with typeof before using in.

Forgetting about optional properties. If a type has the property as optional, it survives in both branches. Write your narrowing logic accordingly or make the property required.

Relying on in for prototype chain properties. in checks the prototype chain, not just own properties. "toString" in {} returns true because everything inherits toString. Be specific with property names.

For other narrowing techniques, see type narrowing with typeof. The instanceof operator handles class-based narrowing for types created with new.

Rune AI

Rune AI

Key Insights

  • The in operator checks if a property exists on an object and acts as a type guard in TypeScript.
  • In the true branch, only union members with that property remain. In the false branch, only members without it remain.
  • Optional properties appear in BOTH branches because TypeScript cannot guarantee their absence.
  • Use in for unions of plain object types where typeof and instanceof do not help.
  • Combine in with other guards like typeof for multi-step narrowing on complex unions.
RunePowered by Rune AI

Frequently Asked Questions

How does the in operator narrow types in TypeScript?

When you write if ('property' in value), TypeScript uses this as a type guard. In the true branch, only union members that have that property (required or optional) remain. In the false branch, only members where the property is missing or optional remain.

When should I use in instead of typeof or instanceof?

Use in when you need to distinguish object types by which properties they have. Use typeof for primitives. Use instanceof for class instances. The in operator is ideal for unions of plain object types with different shapes.

Does in narrowing work with optional properties?

Yes, but with a twist. If a type has an optional property, it appears in BOTH the true and false branches of the in check. TypeScript cannot assume the property is missing just because a value is in the false branch.

Conclusion

The in operator turns a simple property check into a type guard. Use it when you have a union of object types with different shapes and you need to know which shape you have. Remember that optional properties appear in both branches of the check, and combine in with other narrowing techniques when one check is not enough.