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.
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.
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.
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.
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.
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:
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.
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.
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:
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:
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:
| Approach | How it works | Best for |
|---|---|---|
| in operator | Check which properties exist | Ad-hoc unions, external data |
| Discriminated union | Check a shared kind field | Types 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.
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
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.
Frequently Asked Questions
How does the in operator narrow types in TypeScript?
When should I use in instead of typeof or instanceof?
Does in narrowing work with optional properties?
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.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.