cf.completefrontend
Open lab My progress
THE TYPESCRIPT FIELD GUIDE

Narrowing with typeof & truthiness

See how TypeScript reads your if statements: typeof, truthiness, and equality checks turn a value that could be several types into one you can use.

By the end, you can
  • 01
    Narrow with typeofUse a typeof check so TypeScript knows which type a value has inside an if, and avoid the typeof null trap.
  • 02
    Pick the right empty checkChoose between a truthiness check, === and !==, and != null, and know what each one does with 0 and empty text.
  • 03
    Follow the pathsRead how return and throw change a type for the rest of a function, and check it with a ^? query.

One check, a more precise type

A shop page shows prices. Most prices are numbers, but some arrive as text from a form, like "4.50". So the function that shows a price takes either one. In TypeScript you write that as string | number. The | means "or": the price is a string or a number. A type like this is called a union type (the Union types lesson covers it).

A price that may be text (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
function formatPrice(price: string | number) {  return price.toFixed(2);}console.log(formatPrice(3));console.log(formatPrice("4.50"));

Line 1 says price is a string or a number. Line 2 calls toFixed(2), a number method that writes a number with two decimal places, like 3.00. TypeScript marks line 2, on purpose here:

  • Property 'toFixed' does not exist on type 'string | number' means "price might be text, and text has no toFixed". A method has to work for every type in the union before TypeScript allows it.

TypeScript is right. Line 4 prints 3.00, but line 5 passes the text "4.50", and the code stops with a TypeError. The fix is a check you already know from JavaScript, typeof:

The same price, checked with typeofPop out in the code editor (opens in a new tab)TypeScript
function formatPrice(price: string | number) {  if (typeof price === "number") {    return price.toFixed(2);    //     ^?  }  return price;  //     ^?}console.log(formatPrice(3));console.log(formatPrice("4.50"));

Line 2 asks whether price is a number. Inside the if, on line 3, it must be, so TypeScript allows toFixed. The query on line 4 shows the type there: price: number. (A comment like // ^? asks the page "what type is the name my ^ points at?", and the answer appears after it. In your editor, you point at the name instead.) Every number returns on line 3, so only text reaches line 6, and the query on line 7 says price: string. Lines 9 and 10 print 3.00 and 4.50.

You didn't write a new type anywhere. TypeScript read your if and worked out a smaller type for each branch. This is called narrowing.

A card that says string | number goes into the check typeof price === "number". One arrow leads to a card that says number, inside the if on line 3. Another leads to a card that says string, after the if on line 6.price comes instring | numbertypeof price === "number"the check on line 2numberinside the ifstringafter the if
price comes in as string | number. The check on line 2 splits it. Inside the if, only a number is left. After the if, only a string is left, because every number already returned.
Real-life analogyA letter carrier sorting the post

A letter carrier picks up the next item from her bag. It's either a letter or a parcel. She checks its size once. If it's a letter, it goes through the slot. Otherwise it must be a parcel, so she rings the bell. After that one look, she never wonders again what she is holding.

In real life: An item that is a letter or a parcel
In TypeScript: price: string | number
In real life: She checks its size once
In TypeScript: if (typeof price === "number")
In real life: Letters go through the slot
In TypeScript: Inside the if, price is a number
In real life: Whatever is left is a parcel, so she rings the bell
In TypeScript: After the if, price is a string
The main idea

Narrowing is TypeScript following your checks. A value can start with a wide type, like string | number. After a check such as typeof price === "number", TypeScript knows which types are still possible in each branch, and lets you use only what works for them.

typeof checks

In JavaScript, typeof gives back a short piece of text that names a value's kind (the Checking types reliably lesson covers it). It can give eight answers: "string", "number", "bigint", "boolean", "symbol", "undefined", "object", and "function". TypeScript knows what each answer means, so comparing typeof with one of them narrows the value. A check that narrows is called a type guard.

The table below lists the common checks. Read each row as: this check, then what the value can be inside the if.

What each typeof check leaves inside the if
When you writeInside the if, the value isGood to know
typeof value === "string"A string.The empty text "" is a string too.
typeof value === "number"A number.NaN, which means "not a number", is still a number.
typeof value === "boolean"A boolean: true or false.Nothing surprising here.
typeof value === "undefined"undefined.Use it when a value may be missing.
typeof value === "function"A function you can call.Classes count as functions too.
typeof value === "object"An object, an array, or null.typeof null is "object", so null is still possible.

Try it on the price formatter. Each button puts a different check on line 2. Watch the list under the code and the type the query on line 4 shows.

Playground: pick the check on line 2
A price formatter with one checkPop out in the code editor (opens in a new tab)TypeScript
function formatPrice(price: string | number) {  if (typeof price === "number") {    return price.toFixed(2);    //     ^?  }  return price;}console.log(formatPrice(3));console.log(formatPrice("4.50"));
price inside the ifnumber
What happens
TypeScriptNo errors
When it runsprints 3.00, 4.50
Try it yourself

Inside the if, price can only be a number, so toFixed is allowed. The query on line 4 says price: number. Both calls work.

TypeScript checks the code for real each time you pick a check. Only line 2 changes. Reset goes back to the typeof number check.

Only typeof price === "number" leaves a type that has toFixed. With "string", the if holds text instead. With price alone, the if only asks whether price is something other than an empty value like 0 or "". A number and text can both pass, so nothing is ruled out. With "object", nothing can get in, and TypeScript calls that type never: a type with no possible values.

One answer needs care. In JavaScript, typeof null is "object", an old mistake the language kept. TypeScript knows this:

typeof lets null through (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
function tagList(tags: string[] | null) {  if (typeof tags === "object") {    return tags.join(", ");  }  return "No tags";}console.log(tagList(["tea", "green"]));console.log(tagList(null));

Line 1 says a product's tags are a list of text, or null when it has none. Line 2 checks for "object", hoping to find the array. But null passes that check too, so TypeScript marks line 3, on purpose here: 'tags' is possibly 'null' means "tags could still be null here, and you can't read join from null". Line 7 prints tea, green, and line 8 stops with a TypeError. The next section shows a check that does rule out null.

Truthiness checks

An if doesn't need true or false. It turns any value into one. These values count as false and are called falsy: 0, NaN, the empty text "", 0n, null, undefined, and false itself. Every other value is truthy. (Booleans & truthiness covers this.) Checking a value this way is called a truthiness check, and TypeScript narrows with it:

A greeting for a name that may be missingPop out in the code editor (opens in a new tab)TypeScript
function greet(name: string | undefined) {  if (name) {    return `Hello, ${name.toUpperCase()}`;    //               ^?  }  return "Hello, guest";}console.log(greet("Asha"));console.log(greet(undefined));console.log(greet(""));

Line 1 says name is a string or undefined. Line 2 checks if (name). undefined is falsy, so it can't get inside, and the query on line 4 shows name: string. That's why toUpperCase on line 3 is allowed. Line 8 prints Hello, ASHA. Line 9 prints Hello, guest. So does line 10: the empty text is falsy too.

The same kind of check fixes the tag list from the last section:

The tag list, checked for truthinessPop out in the code editor (opens in a new tab)TypeScript
function tagList(tags: string[] | null) {  if (tags) {    return tags.join(", ");    //     ^?  }  return "No tags";}console.log(tagList(["tea", "green"]));console.log(tagList(null));

null is falsy, and every array is truthy, even an empty one. So inside if (tags) the query on line 4 shows tags: string[], and the error is gone. Line 8 prints tea, green and line 9 prints No tags.

The ! operator, which means "not", flips a check. TypeScript follows that too. Here the empty case is handled first:

Handle the empty case first with !Pop out in the code editor (opens in a new tab)TypeScript
function greet(name: string | undefined) {  if (!name) {    return "Hello, guest";  }  return `Hello, ${name.toUpperCase()}`;  //               ^?}console.log(greet("Asha"));

Line 2 reads "if there is no name". That branch returns on line 3, so line 5 only runs with a real name, and the query on line 6 shows name: string. Line 8 prints Hello, ASHA.

A truthiness check has one trap. It removes every falsy value, not only the missing ones. Here an order's quantity may be missing:

A quantity of 0 treated as missingPop out in the code editor (opens in a new tab)TypeScript
function itemsLabel(quantity: number | undefined) {  if (quantity) {    return `${quantity} items`;  }  return "Quantity missing";}console.log(itemsLabel(3));console.log(itemsLabel(0));

TypeScript reports nothing, because the code is allowed. But line 7 prints 3 items and line 8 prints Quantity missing. Zero is a real quantity, yet 0 is falsy, so it goes to the same branch as undefined.

Three values go through if (quantity): 3, 0, and undefined. 3 goes into the if. 0 and undefined both skip it and go to the code after the if, which gives back Quantity missing. A note says 0 is a real quantity.if (quantity)30undefinedif3 itemsafter ifQuantity missing0 is a real quantity
if (quantity) asks "is it truthy?", not "is it a number?". 3 goes into the if. 0 is falsy, so it skips the if with undefined, and an order of 0 items says Quantity missing.

When 0 or "" is a real answer, check for the missing value itself. That's an equality check, which the next section covers:

Only undefined counts as missingPop out in the code editor (opens in a new tab)TypeScript
function itemsLabel(quantity: number | undefined) {  if (quantity !== undefined) {    return `${quantity} items`;    //        ^?  }  return "Quantity missing";}console.log(itemsLabel(3));console.log(itemsLabel(0));

Line 2 now asks quantity !== undefined. The query on line 4 still shows quantity: number, so the type is just as precise. But 0 stays in the if: line 9 prints 0 items.

Equality checks

TypeScript also narrows with the equality operators ===, !==, ==, and != (Equality & sameness explains the difference between them). Comparing with an exact value is the clearest way to rule out null:

A search that may get nullPop out in the code editor (opens in a new tab)TypeScript
function searchFor(query: string | null) {  if (query === null) {    return "Nothing to search";  }  return `Searching for ${query.trim()}`;  //                      ^?}console.log(searchFor("  tea "));console.log(searchFor(null));

Line 1 says query is text or null, for "the visitor hasn't searched yet". Line 2 checks query === null, and that branch returns on line 3. So on line 5 query can only be text: the query on line 6 shows query: string, and trim(), which removes spaces at both ends, is allowed. Line 8 prints Searching for tea and line 9 prints Nothing to search.

Some values can be both null and undefined. A strict check for one of them leaves the other:

!== null leaves undefined (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
function nicknameOf(nickname: string | null | undefined) {  if (nickname !== null) {    return nickname.toUpperCase();  }  return "No nickname";}console.log(nicknameOf("Ash"));console.log(nicknameOf(undefined));

Line 1 says a nickname is text, null, or undefined. Line 2 rules out null only. TypeScript marks line 3, on purpose here: 'nickname' is possibly 'undefined'. Line 7 prints ASH, and line 8 stops with a TypeError. The loose != fixes it, because it treats null and undefined as equal:

!= null rules out null and undefinedPop out in the code editor (opens in a new tab)TypeScript
function nicknameOf(nickname: string | null | undefined) {  if (nickname != null) {    return nickname.toUpperCase();    //     ^?  }  return "No nickname";}console.log(nicknameOf("Ash"));console.log(nicknameOf(undefined));console.log(nicknameOf(null));

Line 2 now says nickname != null. That one check removes both, and the query on line 4 shows nickname: string. Line 8 prints ASH, and lines 9 and 10 both print No nickname. This is the one place many teams use != on purpose.

You can also compare two values. If they're equal, they must share a type:

Two settings that are equal share a typePop out in the code editor (opens in a new tab)TypeScript
function sameSetting(saved: string | number, picked: string | boolean) {  if (saved === picked) {    return saved.toUpperCase();    //     ^?  }  return "Different";}console.log(sameSetting("dark", "dark"));console.log(sameSetting(1, true));

Line 1 takes a saved setting that is text or a number, and a picked one that is text or a boolean. Line 2 checks that they're the same value. The only type both can be is string, so the query on line 4 shows saved: string, and toUpperCase is allowed. Line 8 prints DARK and line 9 prints Different.

The table below compares the checks for a value that may be missing. Read each row as: this check, what it rules out, and what happens to 0 and "".

Four ways to check for a missing value
The checkWhat it rules outWhat happens to 0 and ""
if (quantity)Every falsy value: undefined, null, 0, "", NaN, and false.They skip the if, together with undefined.
if (quantity !== undefined)Only undefined.They stay in the if branch, like any other number.
if (quantity !== null)Only null.They stay in the if branch. undefined stays too.
if (quantity != null)Both null and undefined.They stay in the if branch.

Now write a check yourself. Line 3 reads query.trim(), so line 2 has to rule out null first. Type a check in the box and watch the list under the code as you type.

Playground: write the check that makes line 3 safe
A search that may get nullPop out in the code editor (opens in a new tab)TypeScript
function searchFor(query: string | null) {  if (query.length > 0) {    return `Searching for ${query.trim()}`;  }  return "Nothing to search";}console.log(searchFor("  tea "));console.log(searchFor(""));console.log(searchFor(null));
Line 2 checksquery.length > 0
What happens
TypeScript2 errors: lines 2 and 3
When it runsprints Searching for tea, Nothing to search, then stops with a TypeError
Try it yourself

query.length reads from query before anything has ruled out null, so TypeScript marks lines 2 and 3: 'query' is possibly 'null'. When it runs, the first two calls work, and searchFor(null) stops with a TypeError.

The code is checked by the real TypeScript compiler each time you type. Only line 2 changes. Reset puts query.length > 0 back.

query.length > 0 reads from query before anything checks it, so it fails on line 2 as well. query, query !== null, query != null, and typeof query === "string" all work. They differ only for the empty search: the truthiness check sends it to Nothing to search, and the others let it in. query !== "" rules out the wrong thing, so null still gets through.

TypeScript follows every path

A check doesn't only narrow inside its own if. TypeScript follows every path the code can take, from top to bottom. When a branch ends with return, the rest of the function knows that branch's types are gone. This is called control flow analysis: "control flow" is the order in which code runs.

A discount that narrows step by stepPop out in the code editor (opens in a new tab)TypeScript
function discountLabel(discount: string | number | undefined) {  if (discount === undefined) {    return "No discount";  }  if (typeof discount === "string") {  //         ^?    return `Coupon ${discount.toUpperCase()}`;  }  return `${discount}% off`;  //        ^?}console.log(discountLabel(undefined));console.log(discountLabel("tea10"));console.log(discountLabel(20));

Line 1 takes a discount that is a coupon code (text), a percent (a number), or undefined for none. Line 2 handles undefined and returns. So by line 5, the query on line 6 shows discount: string | number. Line 5 handles text and returns on line 7. So line 9 only sees a number: the query on line 10 says discount: number. Lines 12 to 14 print No discount, Coupon TEA10, and 20% off.

Three cards, each smaller than the one above. string | number | undefined at the start. An arrow down, marked line 2: undefined returns, to string | number. An arrow down, marked line 5: strings return, to number on line 9.string | number | undefinedline 2: undefined returnsstring | numberline 5: strings returnnumberline 9 sees a number
TypeScript follows the function from top to bottom. Each if that returns takes its type away from the rest of the function: undefined after line 2, then string after line 5. Line 9 only sees a number.

Returning early like this is the most common way to narrow. Here a cart badge needs a signed-in user. Line 1 is a type alias: it gives the name User to an object type with a name and a number of cartItems.

Return early when no one is signed inPop out in the code editor (opens in a new tab)TypeScript
type User = { name: string; cartItems: number }; function cartBadge(user: User | null) {  if (user === null) {    return "Sign in";  }  return `Cart: ${user.cartItems} items`;  //               ^?}console.log(cartBadge({ name: "Asha", cartItems: 2 }));console.log(cartBadge(null));

Line 3 takes a User or null. Line 4 handles null, and line 5 returns. So on line 7 the query on line 8 shows user: User. Line 10 prints Cart: 2 items and line 11 prints Sign in.

What matters is that line 5 ends the path. Try other things on line 5 and watch the type on line 7:

Playground: what line 5 does with a null user
A cart badge for a user who may be signed outPop out in the code editor (opens in a new tab)TypeScript
type User = { name: string; cartItems: number }; function cartBadge(user: User | null) {  if (user === null) {    return "Sign in";  }  return `Cart: ${user.cartItems} items`;  //               ^?}console.log(cartBadge({ name: "Asha", cartItems: 2 }));console.log(cartBadge(null));
user on line 7User
What happens
TypeScriptNo errors
When it runsprints Cart: 2 items, Sign in
Try it yourself

return ends the function, so a null user never reaches line 7. TypeScript removes null from the type there: the query says user: User. A visitor sees Sign in.

TypeScript checks the code for real for each choice. Only line 5 changes. Reset goes back to return.

return and throw both end the path, so line 7 sees only a User. A console.log doesn't, so null flows on to line 7 and TypeScript marks it. The same following works inside ? : and after &&: the part after a check sees the narrower type.

A search box on a real page

In the browser, document.querySelector("input") looks for an <input> on the page. It gives back HTMLInputElement | null: the input, or null when there isn't one. So code that reads a search box starts with a check:

Read a search box that may not be therePop out in the code editor (opens in a new tab)TypeScript
function searchText(box: HTMLInputElement | null) {  if (!box) {    return "No search box on this page";  }  const text = box.value.trim();  //           ^?  if (text === "") {    return "Type something to search";  }  return `Searching for ${text}`;} const searchInput = document.createElement("input");searchInput.value = "  green tea ";console.log(searchText(searchInput));console.log(searchText(null));

Line 1 takes the box, or null. Line 2 returns early when there's no box. On line 5, the query on line 6 shows box: HTMLInputElement, so reading box.value is allowed. Line 7 compares the text with "" to catch an empty search. Lines 13 and 14 make an input and fill it, as if the visitor typed "green tea" with extra spaces. Line 15 prints Searching for green tea, and line 16 prints No search box on this page.

Each check sits right where the value is used, and TypeScript follows every one. Now sort some one-line functions: does TypeScript accept each one?

Error or fine?
  • function show(price: string | number) { return price.toFixed(2); }
  • function show(price: string | number) { return typeof price === "number" ? price.toFixed(2) : price; }
  • function search(query: string | null) { return query.trim(); }
  • function search(query: string | null) { return query ? query.trim() : ""; }
  • function tagList(tags: string[] | null) { if (typeof tags === "object") return tags.join(); }
  • function shout(nickname: string | null | undefined) { if (nickname != null) return nickname.toUpperCase(); }
  • function shout(nickname: string | null | undefined) { if (nickname !== null) return nickname.toUpperCase(); }
  • function label(quantity: number | undefined) { if (quantity !== undefined) return quantity.toFixed(0); }
Try it yourself
0 of 8 correct

Sort each function by what TypeScript says about it. Each one is checked on its own.

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

Common mix-ups

  • "Narrowing changes the value." No. The value stays exactly as it was. Only TypeScript's idea of its type changes, and your check is plain JavaScript that runs as usual.
  • "I need a new annotation to narrow a value." No. TypeScript reads the typeof, if, and === checks you already write.
  • "typeof value === "object" means it's a real object." No. typeof null is "object" too, so null is still possible.
  • "if (value) only removes null and undefined." No. It removes every falsy value, including 0 and "", which may be real answers.
  • "!== null also rules out undefined." No. The strict check rules out only null. Use != null to rule out both.
  • "TypeScript forgets my check after the if." No. If the if returns or throws, the rest of the function keeps the narrower type.

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 the if

What type does label have on line 5? Guess first. Then point at it, or press Edit & run and add a // ^? line under it.

Starter codePop out in the code editor (opens in a new tab)TypeScript
function shortLabel(label: string | number) {  if (typeof label === "string") {    return label.slice(0, 3);  }  return label;}console.log(shortLabel("Green tea"));

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

    Exercise 2 · Warm-upFind the line TypeScript marks

    One of these lines gets an error. Which line will TypeScript mark?

    Starter codePop out in the code editor (opens in a new tab)TypeScript
    function totalText(total: string | number) {
      if (typeof total === "string") {
        return total.toUpperCase();
      }
      return total.toUpperCase();
    }

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

      Exercise 3 · PracticeFix a price formatter

      TypeScript marks line 2. Add a typeof check so both calls work and both print two decimal places. What does formatPrice("4.5") print then?

      Starter codePop out in the code editor (opens in a new tab)TypeScript
      function formatPrice(price: string | number) {  return "$" + price.toFixed(2);}console.log(formatPrice(3));console.log(formatPrice("4.5"));

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

        Exercise 4 · PracticeKeep a zero discount

        TypeScript has no complaints. What does the starter print for discountText(0)? Then change line 2 so a 0% discount prints 0% off.

        Starter codePop out in the code editor (opens in a new tab)TypeScript
        function discountText(percent: number | undefined) {  if (percent) {    return `${percent}% off`;  }  return "No discount set";}console.log(discountText(20));console.log(discountText(0));

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

          Exercise 5 · ChallengeEnd the path for a missing order

          Line 7 gets two errors. Fix the function so they go away, and so a missing order gives back a message. What does orderSummary(undefined) print then?

          Starter codePop out in the code editor (opens in a new tab)TypeScript
          type Order = { id: number; items: string[] }; function orderSummary(order: Order | undefined) {  if (order === undefined) {    console.log("No order yet");  }  return `Order ${order.id}: ${order.items.length} items`;}console.log(orderSummary({ id: 7, items: ["tea", "cake"] }));

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

            Exercise 6 · ChallengeRule out both kinds of missing

            Which value can still get past nickname !== null on line 2? Then change line 2 so the error goes away.

            Starter codePop out in the code editor (opens in a new tab)TypeScript
            function displayName(nickname: string | null | undefined, name: string) {  if (nickname !== null) {    return nickname.trim();  }  return name;}console.log(displayName(" Ash ", "Asha"));console.log(displayName(null, "Asha"));

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

              Check your understanding

              For each question, follow the code from top to bottom and ask which types can still reach the line. The quiz shows its code without the checker, so the error list doesn't give the answers away.

              The narrowing quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What does narrowing mean in TypeScript?

                Choose an answer to see the explanation.

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

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                function sizeLabel(size: string | number) {
                  if (typeof size === "number") {
                    return `${size} cm`;
                  }
                  return size;
                }

                Choose an answer to see the explanation.

              3. Question 3 of 7Does TypeScript accept tags.length on line 3?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                function tagCount(tags: string[] | null) {
                  if (typeof tags === "object") {
                    return tags.length;
                  }
                  return 0;
                }

                Choose an answer to see the explanation.

              4. Question 4 of 7What does stockText(0) print?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                function stockText(stock: number | undefined) {
                  if (stock) {
                    return `${stock} left`;
                  }
                  return "Unknown";
                }
                console.log(stockText(0));

                Choose an answer to see the explanation.

              5. Question 5 of 7Which check rules out both null and undefined?

                Choose an answer to see the explanation.

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

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type User = { name: string };
                
                function welcome(user: User | undefined) {
                  if (!user) {
                    return "Welcome, guest";
                  }
                  return `Welcome back, ${user.name}`;
                }

                Choose an answer to see the explanation.

              7. Question 7 of 7What type does saved have on line 3, after saved === picked?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                function sameSize(saved: string | number, picked: string | boolean) {
                  if (saved === picked) {
                    return saved;
                  }
                  return "different";
                }

                Choose an answer to see the explanation.

              Key takeaways

              • A union like string | number only allows what works for every type in it.
              • Narrowing is TypeScript reading your checks to work out a smaller type in each branch. You write no new types.
              • typeof checks narrow to one kind of value, but "object" still lets null through.
              • A truthiness check removes every falsy value, including 0 and "".
              • === null and !== undefined rule out one value. != null rules out both null and undefined.
              • After an if that returns or throws, the rest of the function keeps the narrower type.

              Remember the one-liner.
              Write the check JavaScript needs, and TypeScript follows it to the exact type.

              Coming next: in & instanceof narrowing. It tells object types apart by a property they have or the class that made them.

              CompleteFrontend Clear concepts. Working examples.