in & instanceof narrowing
Tell object types apart in TypeScript with the in operator and instanceof, see what else branches get, and learn how assigning a value narrows a variable.
- 01Check a property with inTell two object types apart by a property only one of them has, and know what happens with optional properties.
- 02Check a class with instanceofNarrow dates, errors, and your own classes with instanceof, and know why it can't check a type alias.
- 03Read what's leftWork out the type in an else branch and after an assignment, and tell the declared type from the narrowed one.
Telling two kinds of object apart
A shop's cart holds two kinds of line: products, with a name and a price, and gift cards, with a code and an amount. A function that takes either one has a parameter typed Product | GiftCard, a union type. To read a price, you first have to find out which kind of line you have. The typeof check from the last lesson can't do it:
type Product = { name: string; price: number };type GiftCard = { code: string; amount: number }; function lineTotal(line: Product | GiftCard) { if (typeof line === "object") { return line.price; } return 0;}Lines 1 and 2 describe the two kinds of line. Line 4 takes either one. Line 5 checks typeof line === "object", but products and gift cards are both objects, so the check is true for both and rules nothing out. Line 6 gets an error, on purpose: Property 'price' does not exist on type 'Product | GiftCard'. In plain words: "this might be a gift card, and a gift card has no price".
JavaScript's in operator asks a different question: does this object have a property with this name? (The rest of the operators covers in.) TypeScript reads that check too:
type Product = { name: string; price: number };type GiftCard = { code: string; amount: number }; function lineTotal(line: Product | GiftCard) { if ("price" in line) { return line.price; // ^? } return line.amount; // ^?}console.log(lineTotal({ name: "Tea", price: 3 }));console.log(lineTotal({ code: "GIFT-20", amount: 20 }));Line 5 asks whether line has a price. Only a Product does, so inside the if, TypeScript treats line as a Product: the query on line 7 shows (parameter) line: Product. Line 6 returns the price. A product returns there, so after the if only a gift card is left, and the query on line 10 shows (parameter) line: GiftCard. Line 9 reads its amount. Line 12 prints 3 for the tea, and line 13 prints 20 for the gift card.
"price" in line looks for a property called price. The tea line has one, so it's a Product inside the if. The gift card has none, so after the if it's a GiftCard.At a lost-and-found desk, a box holds bags and scarves mixed together. To sort them, you look for something only one kind has: a bag has a zip, a scarf doesn't. For a coat, you might read the label sewn inside, which says who made it. Whatever fails the test goes on the other pile.
- In real life: A box of mixed things at a lost-and-found desk
- In TypeScript: A value of type
Product | GiftCard - In real life: "Does it have a zip? Then it's a bag."
- In TypeScript:
"price" in line: it has a price, so it's aProduct - In real life: The label sewn inside says who made it
- In TypeScript:
value instanceof Date: which class made the value - In real life: Whatever has no zip goes on the other pile
- In TypeScript: The
elsebranch gets the types that are left
"price" in line and value instanceof Date are ordinary JavaScript checks that TypeScript reads. An in check keeps the types that have that property. An instanceof check keeps the values made by that class. Each branch gets a narrower type, and the else gets whatever is left.
The in operator
Any property that only one of the types has can tell them apart. Pick the property line 5 looks for, and watch what TypeScript says inside and after the if:
type Product = { name: string; price: number };type GiftCard = { code: string; amount: number }; function lineTotal(line: Product | GiftCard) { if ("price" in line) { return line.price; // ^? } return line.amount; // ^?}console.log(lineTotal({ name: "Tea", price: 3 }));console.log(lineTotal({ code: "GIFT-20", amount: 20 }));3, 20ProductGiftCardNo errorsOnly Product has a price. So inside the if, line is a Product, and after it, a GiftCard. Both reads are allowed, and the cart prints 3 and 20.
"price" and "name" both belong only to Product, so both work. "code" and "amount" belong to GiftCard, so the branches swap: inside the if it's a gift card, and lines 6 and 9 now read the wrong property. TypeScript marks both, and when the code runs, both print undefined.
Things change when a property is optional, written with a ?. An optional property may be there or not, so an in check can't rule its type out:
type EmailContact = { email: string };type PhoneContact = { phone: string };type AnyContact = { email?: string; phone?: string }; function contactKind(contact: EmailContact | PhoneContact | AnyContact) { if ("email" in contact) { return contact; // ^? } return contact; // ^?}Lines 1 to 3 describe three kinds of contact for a sign-up form. EmailContact always has an email, PhoneContact never does, and AnyContact may have either, because both are optional. Line 6 checks for email. The query on line 8 shows the types inside the if: EmailContact | AnyContact. The query on line 11 shows what's left after it: PhoneContact | AnyContact. AnyContact is on both sides, because it may or may not have an email.
An in check also only asks whether the property is there, not what it holds. A property can be there and hold undefined:
type EmailContact = { email: string };type PhoneContact = { phone: string };type AnyContact = { email?: string; phone?: string }; function contactText(contact: EmailContact | PhoneContact | AnyContact) { if ("email" in contact) { return contact.email.toLowerCase(); } return "Call us";}console.log(contactText({ email: "Asha@Shop.com" }));console.log(contactText({ email: undefined, phone: "555-0100" }));Inside the if, contact may be an AnyContact, whose email may be missing. So line 7 gets an error, on purpose: 'contact.email' is possibly 'undefined'. TypeScript is right. Line 11 prints asha@shop.com, but line 12 passes a contact whose email is there and holds undefined. The in check is true, and line 7 throws a TypeError.
instanceof
instanceof asks a different question: was this value made by this class, with new? (Checking types reliably covers how it works.) It's the check to use for JavaScript's own classes, like Date:
function formatDate(value: Date | string) { if (value instanceof Date) { return value.toISOString().slice(0, 10); // ^? } return value; // ^?}console.log(formatDate(new Date(Date.UTC(2026, 9, 24))));console.log(formatDate("2026-10-24"));Line 1 takes an order date that is either a Date or text. Line 2 checks value instanceof Date. Inside, the query on line 4 shows (parameter) value: Date, so line 3 can call toISOString, and slice(0, 10) keeps the year, month, and day. After the if, the query on line 7 shows (parameter) value: string. Line 9 passes a real date and line 10 passes text, and both print 2026-10-24.
It works the same for a class you write yourself:
class GiftCard { code: string; amount: number; constructor(code: string, amount: number) { this.code = code; this.amount = amount; }} function lineTotal(line: GiftCard | number) { if (line instanceof GiftCard) { return line.amount; // ^? } return line;}console.log(lineTotal(new GiftCard("GIFT-20", 20)));console.log(lineTotal(3));Lines 1 to 8 are a GiftCard class (Class basics covers classes in JavaScript). Lines 2 and 3 list its fields with their types: every gift card has a code that is text and an amount that is a number. You'll see more about typing classes in TypeScript classes, later in the course. Line 10 takes a gift card or a plain number. Line 11 checks line instanceof GiftCard, and the query on line 13 shows (parameter) line: GiftCard, so line 12 can read the amount. Line 17 prints 20, and line 18 prints 3.
So why not write instanceof GiftCard when GiftCard is a type alias, like in the overview? Because instanceof runs while the page runs, and types are removed before that:
type GiftCard = { code: string; amount: number }; function isGiftCard(line: object) { return line instanceof GiftCard;}Line 1 makes GiftCard a type alias. Line 4 uses it with instanceof, and gets an error, on purpose: 'GiftCard' only refers to a type, but is being used as a value here. In plain words: "GiftCard is only a type, and types don't exist when the code runs". A class is a real value that stays in the code, which is why instanceof needs one. For plain objects, like data from a server, use in.
The table below compares the two checks. Read each row as a question, then each check's answer.
| Question | in | instanceof |
|---|---|---|
| What does it check? | Whether an object has a property with that name. | Whether a class made the value, with new. |
| What goes on the right? | Any object, like a plain { … } from JSON. | A class, like Date, Error, or one you wrote. |
Does it work for a type alias like type GiftCard = { … }? | Yes. It looks for a property, and plain objects described by a type alias have them. | No. A type alias is gone when the code runs, so there's nothing to check against. |
| Where you'll use it | Plain objects: data from a server, props, settings. | Dates, errors in a catch block, and your own classes. |
Narrowing in else branches
Every check splits the type in two. The if gets the types the check keeps, and the else gets the rest. With a chain of checks, each else has fewer types left:
type Product = { name: string; price: number };type GiftCard = { code: string; amount: number };type Discount = { label: string; percent: number }; function lineText(line: Product | GiftCard | Discount) { if ("price" in line) { return `${line.name}: $${line.price}`; } else if ("amount" in line) { return `Gift card: $${line.amount}`; } else { return `${line.label}: ${line.percent}% off`; // ^? }}console.log(lineText({ name: "Tea", price: 3 }));console.log(lineText({ code: "GIFT-20", amount: 20 }));console.log(lineText({ label: "Welcome", percent: 10 }));Line 3 adds a third kind of line, a Discount. Line 6 checks for a price, so line 7 has a Product. Line 8 checks the rest for an amount, so line 9 has a GiftCard. Nothing else is left for the last else on line 10, and the query on line 12 shows (parameter) line: Discount, so line 11 can read label and percent without a check. Lines 15 to 17 print Tea: $3, Gift card: $20, and Welcome: 10% off.
The else gets a smaller type only when the check can take a member out of a union. When the type isn't a union, like unknown, there's nothing to take out:
function problemText(problem: unknown) { if (problem instanceof Error) { return problem.message; // ^? } return String(problem); // ^?}console.log(problemText(new Error("Offline")));console.log(problemText("Request timed out"));Line 1 takes a problem of type unknown, which means "could be any value, so check before you use it". Inside the if, the query on line 4 shows (parameter) problem: Error, so line 3 can read message. After it, the query on line 7 still shows (parameter) problem: unknown: everything that isn't an Error is still any value at all. String(problem) on line 6 works on any value. Line 9 prints Offline, and line 10 prints Request timed out.
if gets what the check keeps, and the else gets the rest. From Date | string, that leaves string. From unknown, nothing can be taken away, so the else is still unknown.Assignments narrow too
Checks aren't the only thing that narrows. When you store a value in a variable, TypeScript remembers what you stored, until the next assignment:
let coupon: string | number = "TEA10";console.log(coupon.toLowerCase());// ^?coupon = 10;console.log(coupon.toFixed(1));// ^?coupon = true;Line 1 says coupon may hold text or a number, and stores the text "TEA10". The query on line 3 shows let coupon: string, so line 2 can call toLowerCase without a check, and prints tea10. Line 4 stores 10. Now the query on line 6 shows let coupon: number, so line 5 can call toFixed, and prints 10.0.
Line 7 tries to store true and gets an error, on purpose: Type 'boolean' is not assignable to type 'string | number'. The type you wrote on line 1, the declared type, decides what you're allowed to store. What you last stored decides what TypeScript knows is in there now, the narrowed type.
coupon is declared string | number, and that never changes. After line 1 it holds text, so TypeScript treats it as a string. After line 4 it holds a number. Line 7 tries true, which the declared type doesn't allow.Try it: type what line 4 stores, and watch the query on line 6 and the list under the code.
let coupon: string | number = "TEA10";console.log(coupon.toLowerCase()); coupon = 10;console.log(coupon);// ^?10numberNo errorsprints tea10, 10A number is allowed, because the declared type string | number includes it. From line 4 on, TypeScript knows coupon holds a number: the query on line 6 says let coupon: number.
"SAVE5" makes coupon a string again, and Number("5") a number. coupon.length works because coupon still holds text on line 4, and length is a number. true and undefined aren't allowed by the declared type, so line 4 gets an error, and the query shows the declared type, string | number.
The table below compares the two types a variable has. Read each row as a question, then each type's answer.
| Question | The declared type | The narrowed type |
|---|---|---|
| Where does it come from? | The annotation you write: let coupon: string | number. | The last value stored, or the last check. |
| Does it change? | Never. | After every assignment and every check. |
| What does it decide? | Which values you're allowed to store. | What you're allowed to do with the value right now. |
Handling a failed request
A shop page loads its products from a server, and the request can fail. JavaScript lets you throw any value, not only errors, so TypeScript gives the variable in a catch block the type unknown (with strict on):
try { JSON.parse("{ broken");} catch (error) { console.log(error.message);}Line 2 parses broken JSON, which throws a SyntaxError, and line 3 catches it as error. Line 4 reads error.message and gets an error, on purpose: 'error' is of type 'unknown'. In plain words: "this could be anything, so check it first". When it runs, this error happens to be a real SyntaxError, so line 4 prints its message. The check is for the times it isn't.
instanceof is the check for errors, because errors are made by classes. Here a loader can fail with a custom HttpError class (Custom errors covers extends Error), a plain Error, or plain text. Pick what line 10 does:
class HttpError extends Error { status: number; constructor(status: number) { super(`Request failed with status ${status}`); this.status = status; }} function loadProducts(): string[] { throw new HttpError(404);} try { console.log(loadProducts().join(", "));} catch (error) { if (error instanceof HttpError) { console.log(`Server said ${error.status}`); // ^? } else if (error instanceof Error) { console.log(`Something broke: ${error.message}`); // ^? } else { console.log(`Unknown problem: ${String(error)}`); // ^? }}Server said 404throw new HttpError(404);No errorsloadProducts throws an HttpError. The first check matches, and inside it error is an HttpError, so reading error.status is allowed. The page shows Server said 404.
Lines 16 and 19 check from the most specific class to the least, and line 22 takes everything else. An HttpError is also an Error, because its class extends Error, so the HttpError check has to come first. Inside each branch, the query shows the narrowed type: HttpError, then Error, then unknown for anything else. The last branch uses String(error), which works on any value.
type Card = { cardNumber: string }; type PayPal = { email: string }; function payLabel(method: Card | PayPal) { return "email" in method ? method.email : method.cardNumber; }type Card = { cardNumber: string }; type PayPal = { email: string }; function payLabel(method: Card | PayPal) { return method.email; }function dateValue(when: Date | string) { return when instanceof Date ? when.getTime() : when.length; }function dateValue(when: Date | string) { return when.getTime(); }type Card = { cardNumber: string }; function isCard(method: object) { return method instanceof Card; }function errorText(error: unknown) { return error instanceof Error ? error.message : "Unknown problem"; }function errorText(error: unknown) { return error.message; }let orderId: string | number = "A1"; orderId = false;
Sort each line by what TypeScript says about it. Each line is checked on its own, with the lesson's strict settings.
Common mix-ups
- "typeof can tell two object types apart." No. Every object gives
"object". Useinfor a property only one type has, orinstanceoffor a class. - "instanceof works with any type." No. It needs a class, which still exists when the code runs. A type alias or an interface is removed before then.
- "An in check proves the property holds a value." No. It only proves the property is there. An optional property, or one that holds
undefined, still needs a check. - "The else branch gets the declared type back." No. It gets what the check didn't keep:
stringfromDate | string. Fromunknown, that's stillunknown. - "A variable's type never changes after you declare it." No. The declared type stays, but after
coupon = 10TypeScript knowscouponis anumberuntil the next assignment. - "The error in a catch block is always an Error." No. Any value can be thrown, so it's
unknownuntil aninstanceof Errorcheck.
Practice exercises
Each exercise is TypeScript. When you fix code, press Edit & run on the starter code: the errors update as you type, and the console shows what it prints. Try a guess first, then use the hints.
What type does TypeScript give order on line 8? Guess first. Then point at order to check, or press Edit & run and add a // ^? line under it.
type Pickup = { store: string };type Delivery = { address: string; fee: number }; function orderNote(order: Pickup | Delivery) { if ("store" in order) { return `Collect from ${order.store}`; } return `Deliver to ${order.address}`;}console.log(orderNote({ address: "12 Hill Road", fee: 4 }));type Pickup = { store: string };type Delivery = { address: string; fee: number }; function orderNote(order: Pickup | Delivery) { if ("store" in order) { return `Collect from ${order.store}`; } return `Deliver to ${order.address}`; // ^?}console.log(orderNote({ address: "12 Hill Road", fee: 4 }));Only Pickup has a store, so a pickup returns inside the if. Line 8 is only reached by a Delivery, and the query on line 9 shows (parameter) order: Delivery.
TypeScript marks two lines here, but one line causes both. Which one?
type Coupon = { code: string; percent: number };
function couponText(coupon: Coupon | string) {
if (coupon instanceof Coupon) {
return `${coupon.percent}% off`;
}
return coupon;
}type Coupon = { code: string; percent: number }; function couponText(coupon: Coupon | string) { if (typeof coupon === "object") { return `${coupon.percent}% off`; } return coupon;}console.log(couponText({ code: "TEA10", percent: 10 }));Line 4 uses instanceof with a type alias, so TypeScript says 'Coupon' only refers to a type. That check narrows nothing, so line 5 can't read percent from string | Coupon either. A typeof check works, because only the coupon is an object. An in check doesn't: in needs an object, and coupon may be text.
TypeScript marks line 5. Fix it with an in check so a pickup costs nothing. What does the fixed code print for the pickup order?
type Pickup = { store: string };type Delivery = { address: string; fee: number }; function shippingFee(order: Pickup | Delivery) { return order.fee;}console.log(shippingFee({ store: "Market Street" }));console.log(shippingFee({ address: "12 Hill Road", fee: 4 }));type Pickup = { store: string };type Delivery = { address: string; fee: number }; function shippingFee(order: Pickup | Delivery) { if ("fee" in order) { return order.fee; } return 0;}console.log(shippingFee({ store: "Market Street" }));console.log(shippingFee({ address: "12 Hill Road", fee: 4 }));"fee" in order leaves only Delivery inside the if, so order.fee is allowed. A pickup has no fee, so it reaches return 0. The code prints 0, then 4.
An order date is a Date or text. Fix line 2 so both work. What does orderYear("2025-03-01") print after your fix?
function orderYear(placed: Date | string) { return placed.getFullYear();}console.log(orderYear(new Date(2026, 9, 24)));console.log(orderYear("2025-03-01"));function orderYear(placed: Date | string) { if (placed instanceof Date) { return placed.getFullYear(); } return Number(placed.slice(0, 4));}console.log(orderYear(new Date(2026, 9, 24)));console.log(orderYear("2025-03-01"));Inside if (placed instanceof Date), placed is a Date, so getFullYear() is allowed. After it, placed is a string, so slice is allowed. The code prints 2026, then 2025.
Line 2 works, but line 5 has the same call and gets an error. What type does selected have on line 5? Then fix line 5.
let selected: string | number = "Green tea";console.log(selected.toUpperCase()); selected = 3;console.log(selected.toUpperCase());let selected: string | number = "Green tea";console.log(selected.toUpperCase()); selected = 3;console.log(selected.toFixed(0));// ^?After line 4, selected holds 3, so TypeScript narrows it to number, and the query on line 6 shows let selected: number. Numbers have toFixed, so the fixed code prints GREEN TEA, then 3.
TypeScript marks line 8. Fix the catch block so it prints a useful message for any failure. What does it print?
function loadCart(): string[] { throw "Network timeout";} try { console.log(loadCart());} catch (error) { console.log(error.message);}function loadCart(): string[] { throw "Network timeout";} try { console.log(loadCart());} catch (error) { if (error instanceof Error) { console.log(error.message); } else { console.log(String(error)); }}Inside if (error instanceof Error), reading message is allowed. This loader throws the text "Network timeout", which isn't an Error, so the else branch runs and String(error) prints Network timeout. The starter printed undefined, because text has no message.
Check your understanding
For each question, ask: which types does the check keep, and which are left for the other branch? The quiz shows its code without the checker, so the error list doesn't give the answers away.
Question 1 of 7Why can't a typeof check tell a Product from a GiftCard?
Choose an answer to see the explanation.
Question 2 of 7What type does
linehave on line 6?Read the code, then predicttype Product = { name: string; price: number }; type GiftCard = { code: string; amount: number }; function total(line: Product | GiftCard) { if ("amount" in line) { return line.amount; } return line.price; }Choose an answer to see the explanation.
Question 3 of 7Inside the if on line 6, which types can
contactbe?Read the code, then predicttype EmailContact = { email: string }; type PhoneContact = { phone: string }; type AnyContact = { email?: string; phone?: string }; function contactKind(contact: EmailContact | PhoneContact | AnyContact) { if ("email" in contact) { return contact; // ^? } return contact; // ^? }Choose an answer to see the explanation.
Question 4 of 7What does
dateText("Oct 24")print?Read the code, then predictfunction dateText(value: Date | string) { if (value instanceof Date) { return value.toDateString(); } return value.toUpperCase(); } console.log(dateText("Oct 24"));Choose an answer to see the explanation.
Question 5 of 7What does TypeScript say about line 4?
Read the code, then predicttype Coupon = { code: string }; function isCoupon(value: object) { return value instanceof Coupon; }Choose an answer to see the explanation.
Question 6 of 7What type does TypeScript give
orderIdon line 3?Read the code, then predictlet orderId: string | number = "A1"; orderId = 7; console.log(orderId);Choose an answer to see the explanation.
Question 7 of 7What type is
errorin the else branch on line 7?Read the code, then predicttry { JSON.parse("{ broken"); } catch (error) { if (error instanceof SyntaxError) { console.log("Bad JSON"); } else { console.log(error); } }Choose an answer to see the explanation.
Key takeaways
typeofgives"object"for every object, so it can't tell two object types apart."price" in linekeeps the types that have aprice. A type with an optionalpricestays on both sides.value instanceof Datekeeps values made by that class. It needs a class, not a type alias or an interface.- The
elsebranch gets what the check didn't keep. Fromunknown, that's stillunknown. - Storing a value narrows a variable to that value's type. The declared type only decides what you may store.
- A
catchvariable isunknown. Check it withinstanceof Error, most specific class first.
Remember the one-liner.
Ask what only one type has, a property or a class, and TypeScript gives each branch the types that fit.
Coming next: Discriminated unions. Instead of looking for a property only one type has, you give every type the same kind property with a different value, and check that.