Command Palette

Search for a command to run...

Hectal
PHASE 4Beginner ~26 min· topic 4 of 7

Topic 4.4

Narrowing with in and instanceof

In one line

When a union holds objects, typeof isn't enough. The in operator asks "does this object have this property?" and instanceof asks "was this object made by this class?". TypeScript narrows on both.

Think of it like this

At a pet show, one animal is a fish and one is a bird. You don't ask "what is its typeof?", the answer would be "animal" for both. You ask a sharper question: "does it have fins?" or "was it hatched from an egg?". **in asks about a feature the object has; instanceof asks about where the object came from.** Both questions have yes/no answers the compiler can use.

Words you'll meet

New words in this topic, in plain English. Come back here whenever one feels fuzzy.

in operator
"key" in obj is true when the object has a property called key, own or inherited.
instanceof
obj instanceof Cls is true when obj was made by the class Cls (or a subclass).
Prototype chain
The chain of parent objects JavaScript follows to find a property or to answer instanceof.
Class
A blueprint for making objects. new Circle(2) makes an instance of the class Circle.
Instance
One object made from a class.
Realm
A separate JavaScript world, like an iframe, with its own copies of built-ins like Array and Date.
unknown
The type for "could be anything". You must narrow it before using it.
Optional property
A property written name?: type, which the object may or may not have.

Step by step

01Ask about a feature with in

Fish has swim, Bird has fly. "swim" in pet is the question "does this animal swim?".

Inside the if, pet is Fish. After the if returns, only Bird is left, so pet.fly() is allowed. This is narrowing by elimination again (Topic 4.3), driven by a property check.

pets.tswhole filets
function move(pet: Fish | Bird) {
  if ("swim" in pet) {
    return pet.swim(); // pet: Fish
  }
  return pet.fly();    // pet: Bird
}

02Why pet.swim is not a guard

It looks natural to write if (pet.swim). TypeScript refuses to even read it: Property 'swim' does not exist on type 'Bird | Fish'. Reading comes before checking.

in is the way to ask about a property whose existence is the question.

03Ask about origin with instanceof

For class instances, shape instanceof Circle reads the object's prototype chain. If Circle is in it, the answer is yes.

Inside the if, shape is Circle. Afterwards it is Square. If you add a third class later, TypeScript shows the new leftover type, and your last line may stop compiling: a useful warning.

Ask about origin with instanceofdiagram
Rendering diagram…

04Built-in classes

instanceof Date tells a real Date from a string, instanceof Error makes caught values safe to read, instanceof Map separates a Map from a plain object.

In a catch block, e is unknown. Write e instanceof Error ? e.message : String(e) and you cover both real errors and anything strange that was thrown.

05Which guard for which union?

Primitives: typeof. Class instances: instanceof. Plain object shapes with different properties: in. Plain object shapes with a shared tag: a discriminated union (Topic 4.5), which is usually best.

Try it yourself

  1. 1

    Add a third animal

    In the first example add type Frog = { jump(): string }; and change the parameter to Fish | Bird | Frog. Predict what the last pet becomes, and whether pet.fly() still compiles.

    It does not: after the in check, pet is Bird | Frog, and Frog has no fly. Add if ("fly" in pet) and the leftover is Frog.

  2. 2

    A class that does not match

    In the second example add console.log(area(new Date())) and read the error. A Date has neither r nor side, so it's not a Circle | Square.

  3. 3

    Ask about the other member

    In the third example, add if ("b" in p) { const r = p; } else { const s = p; } to g and use // ^? on r and s. r is { b: string }, and s is { a?: number }.

Code & diagrams

in: ask whether the property exists

Each ^? shows the narrowed type at that line: Fish inside the check, Bird after it. Try changing the check to pet.swim in the Break it section.

Console

(nothing was printed)

Expected console output

splash flap
instanceof with your classes and built-ins

RangeError is a subclass of Error, so it passes e instanceof Error. The plain object has the same shape as a Circle but was not made by it, so instanceof says false: it checks origin, not shape.

Console

(nothing was printed)

Expected console output

9.0 3.14
2026 3
too big 5
false
Optional properties, structural matches and the else branch

Three subtleties. Eagle has the same shape as Bird, so p narrows to Eagle (a class instance is checked structurally). In g, the type whose a is optional stays in the true branch and the type without any a is removed. In h, the leftover after the in-check is Bird.

Console

(nothing was printed)

Expected console output

bird fish

Break it on purpose

Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.

Break #1

Check the property instead of asking about it

In the first example, replace if ("swim" in pet) with if (pet.swim).

terminal
$ Types panel
── what you'll see ──
index.ts(5,11): error TS2339: Property 'swim' does not exist on type 'Bird | Fish'.
Property 'swim' does not exist on type 'Bird'.

Myth vs fact

Myth

instanceof works with interfaces and type aliases.

Fact

Those are erased at compile time. instanceof needs a class or constructor function that exists at runtime.

Myth

in checks the value of a property.

Fact

It checks only that the name exists, even if its value is undefined. A property set to undefined is still in the object.

Myth

instanceof compares shapes like TypeScript does.

Fact

At runtime it compares the prototype chain. Same shape, different class gives false.

Pro corner

Extra depth for experienced readers. New to this? Skip it for now and come back later.

  • ▸

    The in guard has a second use since TypeScript 4.9: for an unlisted key on an object, "id" in obj narrows obj to typeof obj & Record<"id", unknown>, so you can safely read an unknown property. It is a way to probe JSON-like data without casts.

  • ▸

    instanceof narrows by the class's instance type and by assignability, not by identity. That is why an Eagle check can narrow Fish | Bird to Eagle: Bird is assignable from Eagle's shape. A class with private members (#x or private) is nominal-like: only its own instances match, because private fields make the shapes unique.

  • ▸

    To find the narrowed type, TypeScript looks at the type of the constructor's prototype property, falling back to its construct signatures. Symbol.hasInstance lets a class override what instanceof means at runtime; since TypeScript 5.3, if that static method is declared with a type predicate return type (static [Symbol.hasInstance](v: unknown): v is PointLike), instanceof narrows by that predicate instead. Cross-realm objects (iframes, Node's vm contexts) fail instanceof: use Array.isArray, Object.prototype.toString or a brand property instead.

Remember this

  1. 1

    The problem this solves: typeof says "object" for almost everything that isn't a primitive, so it can't tell a fish from a bird. You need checks that look at the object itself. typeof can only tell apart the eight primitive-ish kinds. Every object, array, date and class instance reports "object". So for a union of object types (Fish | Bird, Circle | Square, Date | string) you need other tools. This topic teaches the two that look at the object itself.

  2. 2

    **The in operator** is plain JavaScript: "swim" in pet is true when pet has a property called swim, either on the object itself or inherited from its prototype. TypeScript uses it as a guard. In if ("swim" in pet), pet becomes the union members that have (or may have) a swim property. In the else part, it becomes the members that do not.

  3. 3

    This is the most natural fix for the Topic 4.1 problem: pet.swim on Fish | Bird is an error because Bird has no swim, but "swim" in pet is always allowed, since asking about a property is safe even when it's missing. After it passes, pet.swim() works. Remember that checking pet.swim directly is not a guard (TypeScript rejects the read before it can narrow).

  4. 4

    Two details about in. Optional properties: if a member declares a?: number, it *may* have a, so it stays in the true branch (and in the false branch). TypeScript can't know. Primitives throw: "x" in 5 is a runtime TypeError (the right side must be an object), and TypeScript also rejects it at compile time. Narrow with typeof x === "object" first if the value could be a primitive, and remember typeof null is "object".

  5. 5

    **The instanceof operator** checks whether an object was created by a class or constructor function: shape instanceof Circle is true when Circle.prototype appears in the object's prototype chain (the linked list of parents every object has, see the JavaScript course). TypeScript narrows shape to Circle inside the check, and to the remaining members after it. This is the natural guard for class instances.

  6. 6

    instanceof works with built-in classes too, and these are the ones you'll use daily: value instanceof Date, Error, RegExp, Map, Set, Promise, Array and Uint8Array. The most common use is in a catch block: the caught value has type unknown (the safe "could be anything" type, enabled by useUnknownInCatchVariables which strict turns on, since TypeScript 4.4), so you write if (e instanceof Error) { e.message } to read it safely. A plain object like { r: 2 } is not an instance of anything you wrote, so plain instanceof Circle is false.

  7. 7

    Limits of instanceof: it only exists at runtime for classes and constructor functions. Interfaces and type aliases are erased, so pet instanceof Fish where Fish is a type is a compile error (the name only refers to a type). It also fails for objects from another realm (an iframe or another window has its own Array and Date; use Array.isArray for arrays). And the narrowing follows TypeScript's structural typing (Topic 3.5: types match by shape, not by name): if a class has the same shape as another union member, the narrowed type can be that class (the pro section shows it).

Explain it without notes

01

How do you tell apart members of a union of object types when typeof only says "object"?

02

Why can't you write if (pet.swim) to narrow Fish | Bird, and what do you write instead?

03

What does instanceof actually check at runtime, and what are its limits?

04

How do you handle a caught error safely in TypeScript?

Practice

01

Write type Square = { side: number }, type Rect = { width: number; height: number } and a function area(s: Square | Rect): number that uses in.

02

Write a function toText(x: Date | string | number): string that returns an ISO string for a date, the text itself for a string, and the number as text. Use instanceof and typeof.

03

Write a function message(e: unknown): string for a catch block that returns e.message for an Error, the text itself for a string, and "unknown error" otherwise.

Trade-offs

  • ↔

    in is flexible but stringly-typed: a typo in "swin" is not an error, because any string is allowed on the left. A tag property (Topic 4.5) lets the compiler check the name.

  • ↔

    instanceof couples your type checks to classes. If your data comes from JSON it is plain objects, never class instances, so instanceof will always say false; use in or a tag.

Done when you can

  • I can narrow a union of objects with the in operator.

  • I can explain why pet.swim is not a guard and "swim" in pet is.

  • I can narrow class instances and built-ins with instanceof.

  • I can write a safe catch block with instanceof Error.

  • I can say why instanceof can't be used with interfaces.