"For classes, instanceof. For property existence, in. Both narrow exactly what they say."
instanceof for class instances
x instanceof Foo at runtime checks whether x's prototype chain includes Foo.prototype. TypeScript reads this as narrowing: inside the if, x is type Foo. Outside, x is the union minus Foo. This works for any class — built-in (Date, Error, Array, Map) or user-defined.
The classic use: distinguishing class-instance variants of a union. x: Date | string can be narrowed with if (x instanceof Date) — inside, x.toISOString(); outside, x.toUpperCase().
in for property existence
'prop' in x at runtime checks whether x has a property named 'prop' (anywhere in its prototype chain). TypeScript reads this as narrowing: inside the if, x is the variant of the union that has prop. Outside, x is the variants without it.
The in operator is the cleanest way to narrow on shape when the variants don't share a discriminator field. x: { name: string } | { id: number } can be narrowed with if ('name' in x).
When to choose which
instanceof: when the variants are class instances (Date vs Error vs RegExp). Class identity is the discriminator.
in: when the variants are plain object shapes without a shared discriminator. Property existence is the discriminator.
typeof: primitives.
discriminator equality: when shapes share a kind/type field with literal values — the cleanest, most explicit option.