ESC

Type to search the knowledge base.

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.

beginner3 min read
  • 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

  1. Primitives — 'x' in 42 throws at runtime; narrow to object first.
  2. Arrays — 'length' in arr is true; don’t use in for sparse membership of values.
  3. Prototype pollution keys — rare, but own-property checks differ.
  4. Classes — methods on the prototype are in the 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.”

Further reading

Related guides