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.
| Guard | Best for | Survives compilation as |
|---|---|---|
typeof | primitive unions | a runtime string check |
instanceof | class instances | a prototype-chain check |
in | object shapes / interfaces | a 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
inoperator 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 — combineinwith 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
into narrow unions of object/interface shapes that differ by which keys they carry. - Prefer
inoverinstanceoffor 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/typefield when you control the types. - Combine
inwithtypeofand anullcheck when narrowingunknownpayloads. - Remember
innarrows on declared shape, not runtime values, so guard the value when it matters.