cf.completefrontend
Open lab My progress
THE TYPESCRIPT FIELD GUIDE

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.

By the end, you can
  • 01
    Check a property with inTell two object types apart by a property only one of them has, and know what happens with optional properties.
  • 02
    Check a class with instanceofNarrow dates, errors, and your own classes with instanceof, and know why it can't check a type alias.
  • 03
    Read 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:

typeof can't tell two objects apart (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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:

An in check tells them apartPop out in the code editor (opens in a new tab)TypeScript
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.

The question "price" in line, over two carts. The tea cart holds name and price cards, so the answer is true and line is a Product inside the if. The gift cart holds code and amount cards, so the answer is false and line is a GiftCard after the if."price" in linenamepriceteatruea Product, inside the ifcodeamountgiftfalsea GiftCard, after the if
"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.
Real-life analogySorting a lost-and-found box

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 a Product
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 else branch gets the types that are left
The main idea

"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:

Playground: pick the property line 5 looks for
A cart line total with an in checkPop out in the code editor (opens in a new tab)TypeScript
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 }));
Console output3, 20
What happens
Inside the ifProduct
After the ifGiftCard
TypeScriptNo errors
Try it yourself

Only 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.

TypeScript checks the code for real each time you pick a property. Only line 5 changes. Reset goes back to price.

"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:

An optional property stays on both sidesPop out in the code editor (opens in a new tab)TypeScript
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:

A property that's there but undefined (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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:

A date that may arrive as a Date or as textPop out in the code editor (opens in a new tab)TypeScript
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:

instanceof with your own classPop out in the code editor (opens in a new tab)TypeScript
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:

instanceof with a type alias (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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.

in and instanceof, side by side
Questionininstanceof
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 itPlain 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:

Three kinds of cart line, checked one by onePop out in the code editor (opens in a new tab)TypeScript
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:

instanceof on an unknown valuePop out in the code editor (opens in a new tab)TypeScript
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.

Top row: Date | string goes through value instanceof Date. The if gets Date, the else gets string. Bottom row: unknown goes through problem instanceof Error. The if gets Error, the else is still unknown.ifelseDate | stringvalue instanceof DateDatestringunknownproblem instanceof ErrorErrorunknownnothing takenaway
The 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:

A coupon that holds text, then a number (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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.

Three boxes called coupon, all declared string | number. After line 1 the box holds TEA10, a string. After line 4 it holds 10, a number. Line 7 tries to put true in, which is not allowed.let coupon: string | number"TEA10"couponafter line 1string10couponafter line 4numbertruecouponline 7not allowed
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.

Playground: store a new coupon on line 4
A coupon that is text or a numberPop out in the code editor (opens in a new tab)TypeScript
let coupon: string | number = "TEA10";console.log(coupon.toLowerCase()); coupon = 10;console.log(coupon);//          ^?
Line 4 stores10
What happens
coupon on line 5number
TypeScriptNo errors
When it runsprints tea10, 10
Try it yourself

A 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.

The code is checked by the real TypeScript compiler each time you type. Only line 4 changes. Reset puts 10 back.

"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.

The declared type and the narrowed type
QuestionThe declared typeThe 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):

A catch block's error is unknown (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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:

Playground: what loadProducts does on line 10
Loading products that may failPop out in the code editor (opens in a new tab)TypeScript
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)}`);    //                                     ^?  }}
Console outputServer said 404
Line 10
Doesthrow new HttpError(404);
TypeScriptNo errors
Try it yourself

loadProducts 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.

TypeScript checks the code for real for each choice, and the output is what this code prints. Only line 10 changes. Reset goes back to HttpError.

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.

Error or fine?
  • 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;
Try it yourself
0 of 8 correct

Sort each line by what TypeScript says about it. Each line is checked on its own, with the lesson's strict settings.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Common mix-ups

  • "typeof can tell two object types apart." No. Every object gives "object". Use in for a property only one type has, or instanceof for 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: string from Date | string. From unknown, that's still unknown.
  • "A variable's type never changes after you declare it." No. The declared type stays, but after coupon = 10 TypeScript knows coupon is a number until the next assignment.
  • "The error in a catch block is always an Error." No. Any value can be thrown, so it's unknown until an instanceof Error check.

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.

Exercise 1 · Warm-upRead the type after an in check

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.

Starter codePop out in the code editor (opens in a new tab)TypeScript
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 }));

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · Warm-upFind the line that causes both errors

    TypeScript marks two lines here, but one line causes both. Which one?

    Starter codePop out in the code editor (opens in a new tab)TypeScript
    type Coupon = { code: string; percent: number };
    
    function couponText(coupon: Coupon | string) {
      if (coupon instanceof Coupon) {
        return `${coupon.percent}% off`;
      }
      return coupon;
    }

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeCharge a fee only for deliveries

      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?

      Starter codePop out in the code editor (opens in a new tab)TypeScript
      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 }));

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeRead the year from a date or text

        An order date is a Date or text. Fix line 2 so both work. What does orderYear("2025-03-01") print after your fix?

        Starter codePop out in the code editor (opens in a new tab)TypeScript
        function orderYear(placed: Date | string) {  return placed.getFullYear();}console.log(orderYear(new Date(2026, 9, 24)));console.log(orderYear("2025-03-01"));

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeFollow a variable through two assignments

          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.

          Starter codePop out in the code editor (opens in a new tab)TypeScript
          let selected: string | number = "Green tea";console.log(selected.toUpperCase()); selected = 3;console.log(selected.toUpperCase());

          Answer, then press Check. Spacing and letter case don’t matter.

            Exercise 6 · ChallengeReport any failure from a loader

            TypeScript marks line 8. Fix the catch block so it prints a useful message for any failure. What does it print?

            Starter codePop out in the code editor (opens in a new tab)TypeScript
            function loadCart(): string[] {  throw "Network timeout";} try {  console.log(loadCart());} catch (error) {  console.log(error.message);}

            Answer, then press Check. Spacing and letter case don’t matter.

              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.

              The in & instanceof quiz · 7 questionsScore: first tries count
              1. Question 1 of 7Why can't a typeof check tell a Product from a GiftCard?

                Choose an answer to see the explanation.

              2. Question 2 of 7What type does line have on line 6?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type 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.

              3. Question 3 of 7Inside the if on line 6, which types can contact be?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                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;
                  //     ^?
                }

                Choose an answer to see the explanation.

              4. Question 4 of 7What does dateText("Oct 24") print?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                function 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.

              5. Question 5 of 7What does TypeScript say about line 4?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type Coupon = { code: string };
                
                function isCoupon(value: object) {
                  return value instanceof Coupon;
                }

                Choose an answer to see the explanation.

              6. Question 6 of 7What type does TypeScript give orderId on line 3?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                let orderId: string | number = "A1";
                orderId = 7;
                console.log(orderId);

                Choose an answer to see the explanation.

              7. Question 7 of 7What type is error in the else branch on line 7?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                try {
                  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

              • typeof gives "object" for every object, so it can't tell two object types apart.
              • "price" in line keeps the types that have a price. A type with an optional price stays on both sides.
              • value instanceof Date keeps values made by that class. It needs a class, not a type alias or an interface.
              • The else branch gets what the check didn't keep. From unknown, that's still unknown.
              • Storing a value narrows a variable to that value's type. The declared type only decides what you may store.
              • A catch variable is unknown. Check it with instanceof 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.

              CompleteFrontend Clear concepts. Working examples.