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 objis true when the object has a property calledkey, own or inherited.- instanceof
obj instanceof Clsis true whenobjwas made by the classCls(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 classCircle. - Instance
- One object made from a class.
- Realm
- A separate JavaScript world, like an iframe, with its own copies of built-ins like
ArrayandDate. - 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.
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.
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
Add a third animal
In the first example add
type Frog = { jump(): string };and change the parameter toFish | Bird | Frog. Predict what the lastpetbecomes, and whetherpet.fly()still compiles.It does not: after the
incheck,petisBird | Frog, andFroghas nofly. Addif ("fly" in pet)and the leftover isFrog. - 2
A class that does not match
In the second example add
console.log(area(new Date()))and read the error. ADatehas neitherrnorside, so it's not aCircle | Square. - 3
Ask about the other member
In the third example, add
if ("b" in p) { const r = p; } else { const s = p; }togand use// ^?onrands.ris{ b: string }, andsis{ a?: number }.
Code & diagrams
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 flapRangeError 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
falseThree 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 fishBreak 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).
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
inguard has a second use since TypeScript 4.9: for an unlisted key on an object,"id" in objnarrowsobjtotypeof obj & Record<"id", unknown>, so you can safely read anunknownproperty. It is a way to probe JSON-like data without casts. - ▸
instanceofnarrows by the class's instance type and by assignability, not by identity. That is why anEaglecheck can narrowFish | BirdtoEagle:Birdis assignable fromEagle's shape. A class with private members (#xorprivate) 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
prototypeproperty, falling back to its construct signatures.Symbol.hasInstancelets a class override whatinstanceofmeans 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),instanceofnarrows by that predicate instead. Cross-realm objects (iframes, Node'svmcontexts) failinstanceof: useArray.isArray,Object.prototype.toStringor a brand property instead.
Remember this
- 1
The problem this solves:
typeofsays "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.typeofcan 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
**The
inoperator** is plain JavaScript:"swim" in petistruewhenpethas a property calledswim, either on the object itself or inherited from its prototype. TypeScript uses it as a guard. Inif ("swim" in pet),petbecomes the union members that have (or may have) aswimproperty. In theelsepart, it becomes the members that do not. - 3
This is the most natural fix for the Topic 4.1 problem:
pet.swimonFish | Birdis an error becauseBirdhas noswim, but"swim" in petis always allowed, since asking about a property is safe even when it's missing. After it passes,pet.swim()works. Remember that checkingpet.swimdirectly is not a guard (TypeScript rejects the read before it can narrow). - 4
Two details about
in. Optional properties: if a member declaresa?: number, it *may* havea, so it stays in the true branch (and in the false branch). TypeScript can't know. Primitives throw:"x" in 5is a runtimeTypeError(the right side must be an object), and TypeScript also rejects it at compile time. Narrow withtypeof x === "object"first if the value could be a primitive, and remembertypeof nullis"object". - 5
**The
instanceofoperator** checks whether an object was created by a class or constructor function:shape instanceof CircleistruewhenCircle.prototypeappears in the object's prototype chain (the linked list of parents every object has, see the JavaScript course). TypeScript narrowsshapetoCircleinside the check, and to the remaining members after it. This is the natural guard for class instances. - 6
instanceofworks with built-in classes too, and these are the ones you'll use daily:value instanceof Date,Error,RegExp,Map,Set,Promise,ArrayandUint8Array. The most common use is in acatchblock: the caught value has typeunknown(the safe "could be anything" type, enabled byuseUnknownInCatchVariableswhichstrictturns on, since TypeScript 4.4), so you writeif (e instanceof Error) { e.message }to read it safely. A plain object like{ r: 2 }is not an instance of anything you wrote, soplain instanceof Circleisfalse. - 7
Limits of
instanceof: it only exists at runtime for classes and constructor functions. Interfaces and type aliases are erased, sopet instanceof FishwhereFishis atypeis 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 ownArrayandDate; useArray.isArrayfor 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
How do you tell apart members of a union of object types when typeof only says "object"?
Why can't you write if (pet.swim) to narrow Fish | Bird, and what do you write instead?
What does instanceof actually check at runtime, and what are its limits?
How do you handle a caught error safely in TypeScript?
Practice
Write type Square = { side: number }, type Rect = { width: number; height: number } and a function area(s: Square | Rect): number that uses in.
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.
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
- ↔
inis 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. - ↔
instanceofcouples your type checks to classes. If your data comes from JSON it is plain objects, never class instances, soinstanceofwill always sayfalse; useinor a tag.
Done when you can
I can narrow a union of objects with the
inoperator.I can explain why
pet.swimis not a guard and"swim" in petis.I can narrow class instances and built-ins with
instanceof.I can write a safe
catchblock withinstanceof Error.I can say why
instanceofcan't be used with interfaces.