Narrowing with in Operator
Use the in operator to narrow unions by property presence — object shapes, optional fields, and when in is the wrong tool.
- typescript
- narrowing-with
When a value is a union of object shapes, TypeScript can narrow with the JavaScript in operator: if a property key exists on the value’s type, the true branch refines to members that declare that key.
type Success = { data: string };
type Failure = { error: string };
function handle(result: Success | Failure) {
if ('data' in result) {
console.log(result.data);
} else {
console.log(result.error);
}
}
Docs: Narrowing — The in operator narrowing, Type narrowing.
Mental model
in at runtime checks the prototype chain for a property name. In the type system, TypeScript uses it as: “keep union members for which this key is a known property.”
type Cat = { meow: () => void };
type Dog = { bark: () => void };
function speak(pet: Cat | Dog) {
if ('meow' in pet) pet.meow();
else pet.bark();
}
Prefer discriminants when you control the shape
type Result =
| { kind: 'ok'; data: string }
| { kind: 'err'; error: string };
function handle(r: Result) {
if (r.kind === 'ok') console.log(r.data);
else console.log(r.error);
}
Discriminants (kind / status) are clearer and don’t depend on accidental shared property names. Use in when shapes come from elsewhere or tags aren’t available.
Optional properties caveats
type A = { id: string; label?: string };
type B = { id: string; error: string };
function f(x: A | B) {
if ('label' in x) {
// x is A — but label may still be undefined
console.log(x.label?.toUpperCase());
}
}
in means the property can exist on the type, not that it’s a non-undefined string.
Shared keys don’t narrow enough
type A = { type: string; a: number };
type B = { type: string; b: number };
function f(x: A | B) {
if ('type' in x) {
// still A | B — both have type
}
}
Narrow on a key unique to one side, or use a literal discriminant.
in vs hasOwnProperty vs optional chaining
| Check | Narrows TS? | Notes |
|---|---|---|
'k' in obj |
Yes (unions of objects) | Includes prototype chain |
obj.hasOwnProperty('k') |
No automatic narrow | Own keys only |
obj.k !== undefined |
Sometimes | Depends on optional typing |
For own-key checks with narrowing, user-defined guards help:
function hasOwn<O extends object, K extends PropertyKey>(
obj: O,
key: K,
): obj is O & Record<K, unknown> {
return Object.prototype.hasOwnProperty.call(obj, key);
}
DOM / unknown data
function readError(payload: unknown): string | undefined {
if (typeof payload === 'object' && payload !== null && 'message' in payload) {
const msg = (payload as { message: unknown }).message;
return typeof msg === 'string' ? msg : undefined;
}
}
Still validate value types — in only helps with key presence.
Footguns
- Primitives —
'x' in 42throws at runtime; narrow to object first. - Arrays —
'length' in arris true; don’t useinfor sparse membership of values. - Prototype pollution keys — rare, but own-property checks differ.
- Classes — methods on the prototype are
inthe instance.
function safe(x: unknown) {
if (typeof x === 'object' && x !== null && 'id' in x) {
// ok
}
}
Interview out-loud answer
“in narrows object unions by property name. I prefer discriminant fields when I design the type. With in, I still handle optionality and validate value types for unknown data. Always exclude null before in.”
Related on this site
- Type narrowing
- Type guards typeof and instanceof
- Discriminated unions for UI state
- unknown vs any
- never type and exhaustiveness
Further reading
Related guides
- Basic Types and AnnotationsPrimitives, arrays, objects, and function annotations TypeScript actually checks — plus where inference is enough and where it is not.
- Branded Types for IDsNominal-style UserId vs OrderId in TypeScript — prevent ID mixups at compile time with brands, parsers, and form boundaries.
- Conditional Types IntroT extends U ? X : Y — how TypeScript picks types from conditions, distributes over unions, and powers utility types you already use.
- Declaration Files and DefinitelyTypedHow .d.ts files describe JS to TypeScript, when to use @types packages, module augmentation, and writing minimal ambient types for untyped libs.
- Discriminated Unions for UI StateModel loading, success, and error as mutually exclusive variants so TypeScript and your UI cannot show impossible states.