cf.completefrontend
Open lab My progress
THE TYPESCRIPT FIELD GUIDE

Habits of Good TypeScript

The habits that make TypeScript catch more: let inference work, type the edges, prefer unknown to any, narrow instead of casting, and model states with unions.

By the end, you can
  • 01
    Write types where they countLet TypeScript work out types inside your code, and write them at the edges: parameters, exported return types, and data from outside.
  • 02
    Keep the checker onUse unknown, narrowing, and real checks instead of any, as, and !, so mistakes are still reported.
  • 03
    Model what can really happenReplace bags of optional flags with unions, check every case with never, and keep types small, precise, and fast to check.

Habits that let TypeScript help

TypeScript can only check what your types tell it. Here is a cart total with a typo in it, prise instead of price, and the type any on its items:

A cart total with items: anyPop out in the code editor (opens in a new tab)TypeScript
function cartTotal(items: any) {  return items.reduce((sum: number, item: any) => sum + item.price, 0);} const total = cartTotal([{ name: "Tea", prise: 3 }]);console.log(total);

Line 1 says items is any, which means "anything at all, don't check it". Line 2 adds up item.price for each item. Line 5 passes an item with prise. There are no errors, and line 6 prints NaN, because item.price is undefined. Now the same code with a real type, and an error on purpose:

The same total with CartItem[] (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type CartItem = { name: string; price: number }; function cartTotal(items: CartItem[]) {  return items.reduce((sum, item) => sum + item.price, 0);} const total = cartTotal([{ name: "Tea", prise: 3 }]);console.log(total);

Line 1 describes a CartItem. Line 3 says items is a list of them, and line 4 needs no types at all: TypeScript works out sum and item from line 3. Line 7 is marked: Object literal may only specify known properties, but 'prise' does not exist in type 'CartItem'. Did you mean to write 'price'? The code still prints NaN if you run it, but you'd fix it before anyone does.

Left: an editor shows cartTotal(items: any) and no marks; under it, prints NaN. Right: an editor shows cartTotal(items: CartItem[]) and the typo prise marked; under it, found before it runs.items: anyeditorcartTotal(items: any)no marksprints NaNitems: CartItem[]editorcartTotal(items: CartItem[])prise is markedfound before it runs
The same typo, prise, in both. With items: any, TypeScript has nothing to check, and the total is NaN. With CartItem[], it marks the typo while you type.

Both versions are valid TypeScript. The difference is a habit. This lesson collects the habits that keep TypeScript checking, each with a before and an after, and links to the lesson that teaches it in full.

Real-life analogyA smoke alarm in the kitchen

A smoke alarm beeps when you burn the toast. You could take the battery out, and the beeping would stop. The toast would still be burning. Opening the window clears the smoke, and the alarm stops because there's nothing to warn about. Good habits are the second kind: they deal with the problem, and the alarm keeps working for the next one.

In real life: The alarm goes off when there's smoke
In TypeScript: TypeScript reports a value that doesn't fit
In real life: Taking the battery out stops the noise
In TypeScript: any, as, and ! stop the error
In real life: Opening a window clears the smoke
In TypeScript: A check like ?? 0 handles the value
In real life: An alarm in every room, not just one
In TypeScript: Types at every edge, not just a few
The main idea

Good TypeScript keeps the checker on. Let TypeScript work out types inside your code, and write them at the edges. Handle a value that may be wrong instead of silencing the error. Make types say exactly what can happen, so wrong states can't be written at all.

Let inference work, annotate the edges

You don't need to write a type on every name. TypeScript works out the type from the value, which is called inference. This code writes every type by hand:

Every type written by handPop out in the code editor (opens in a new tab)TypeScript
const taxRate: number = 0.2;const items: string[] = ["Tea", "Cake"];const count: number = items.length;console.log(count);

Lines 1 to 3 each say a type the value already shows: 0.2 is a number, the list holds strings, and .length is a number. Line 4 prints 2. Here it is with no annotations, and a query under each name:

The same code, with the types worked outPop out in the code editor (opens in a new tab)TypeScript
const taxRate = 0.2;//    ^?const items = ["Tea", "Cake"];//    ^?const count = items.length;//    ^?console.log(count);

The query on line 2 answers const taxRate: 0.2, which is even more exact: a const can never change, so its type is the value itself. Line 4 answers const items: string[] and line 6 const count: number. Same checks, less to read. (Annotations and inference covers the rules.)

An annotation can even make a type worse. Switch line 4 between three versions:

Playground: annotate the status, or let TypeScript work it out
Shipping an order with a statusPop out in the code editor (opens in a new tab)TypeScript
type Status = "paid" | "shipped";function ship(status: Status) { console.log(`Order is ${status}`); } const status: string = "paid";ship(status);//   ^?
The type of statusstring
What happens
TypeScript1 error: line 5
When it runsprints Order is paid
Try it yourself

: string tells TypeScript to forget that the value is exactly "paid". Line 5 then fails: Argument of type 'string' is not assignable to parameter of type 'Status'. The annotation made the type worse.

TypeScript checks the code for real. Each button changes only line 4, and the query on line 6 shows the type of status. Reset goes back to : string.

With : string, line 5 is marked: Argument of type 'string' is not assignable to parameter of type 'Status'. The annotation threw away the fact that the value is exactly "paid" (Literal types). With no annotation, TypeScript keeps it, and the error is gone.

So where do you write types? At the edges: the places where values come in or go out, and where TypeScript has nothing to learn from.

  • Function parameters. A parameter has no value until someone calls the function.
  • The return type of a function other files use, so a mistake inside it is marked inside it.
  • Data from outside your code, like a server reply (more on that below).
Types on the edges of summarizePop out in the code editor (opens in a new tab)TypeScript
type Summary = { count: number; total: number }; export function summarize(prices: number[]): Summary {  const total = prices.reduce((sum, price) => sum + price, 0);  //    ^?  return { count: prices.length, total };} console.log(summarize([3, 4]));

Line 1 names the result's type. Line 3 writes the two edges: the parameter prices: number[] and the return type Summary. Line 4 has no annotation, and the query on line 5 shows const total: number. Line 6 returns the count and the total, and line 9 prints { count: 2, total: 7 }. If line 6 spelled totl, the return type would mark it right there, as Reading type errors showed.

A file summary.ts with three parts of summarize: the parameter prices: number[], marked edge, write the type; total inside, marked inside, worked out; and the return type Summary, marked edge, write the type.summary.ts(prices: number[])const total = …): Summaryedge: write the typeinside: worked outedge: write the type
summarize from the example. You write the types where data comes in and goes out. TypeScript works out the rest, like total.

Handle a value instead of silencing the error

Three things turn TypeScript's checks off: the type any, a type assertion like value as Product, and the non-null assertion value!. Each one tells TypeScript "trust me". None of them runs any code: they're removed with the types.

Start with any. When you don't know a value's type yet, like the result of JSON.parse, use unknown instead. It means "could be anything, so check it first". This reply is used without a check, and TypeScript marks it on purpose:

A reply typed unknown (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
const data: unknown = JSON.parse('{"name":"Tea","price":3}');console.log(data.price);

Line 1 parses the reply and gives it the type unknown. Line 2 reads data.price, and TypeScript says 'data' is of type 'unknown': prove what it is first. With any, line 2 would pass, and so would data.prise. Add the proof:

The reply, checked before it's usedPop out in the code editor (opens in a new tab)TypeScript
const data: unknown = JSON.parse('{"name":"Tea","price":3}');if (typeof data === "object" && data !== null && "price" in data) {  console.log(data.price);}

Line 2 checks that data is an object, isn't null, and has a price. Inside the if, TypeScript allows data.price, and line 3 prints 3. This is narrowing: a check that TypeScript understands and that really runs. any, unknown, and never compares the two types.

Now ! and as. A Map gives back undefined for a key it doesn't have (Map and Set). Try four ways to deal with that on line 5:

Playground: silence the error, or handle the value
The next count of an item in stockPop out in the code editor (opens in a new tab)TypeScript
const stock = new Map([["tea", 5]]); function nextCount(id: string) {  const count = stock.get(id);  return count + 1;}console.log(nextCount("cake"));
Console outputNaN
What happens
TypeScript1 error: line 5
Try it yourself

TypeScript marks line 5: 'count' is possibly 'undefined'. The stock has no cake, so get gives back undefined, and undefined + 1 is NaN.

TypeScript checks the code for real, and the output is what it prints. Each button changes only line 5. Reset goes back to count + 1.

Read the output, not just the error list. count! + 1 and (count as number) + 1 both have no errors and still print NaN: they changed what TypeScript believes, not what the code does. Only (count ?? 0) + 1 handles the missing count, and prints 1 (Nullish coalescing). Use ! and as only when you know something TypeScript can't, and say why in a comment. Type assertions covers the few places they belong.

Make wrong states impossible

A product list loads from a server. It can be loading, failed, or done. A common first type uses a flag and two optional properties:

A request typed with flagsPop out in the code editor (opens in a new tab)TypeScript
type ProductsRequest = {  loading: boolean;  error?: string;  products?: string[];}; const request: ProductsRequest = { loading: true, error: "Timed out", products: ["Tea"] };console.log(request.loading);

Lines 1 to 5 say a request has a loading flag, maybe an error, and maybe some products. Line 7 makes a request that is loading, failed, and done at the same time. That can't happen, but the type allows it, so there's no error. Every piece of code that reads a request has to guess which mix it got. Line 8 prints true.

Instead, list the states. Give each one a status with its own exact value, and only the properties that state has. This is a discriminated union:

A request typed with its states (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type ProductsRequest =  | { status: "loading" }  | { status: "error"; error: string }  | { status: "success"; products: string[] }; const request: ProductsRequest = { status: "loading", error: "Timed out" };console.log(request.status);

Lines 1 to 4 say a request is exactly one of three things. Only an error has an error, and only a success has products. Line 6 tries the impossible mix again, and TypeScript marks it: Object literal may only specify known properties, and 'error' does not exist in type '{ status: "loading"; }'. A loading request has no error. The wrong state can't be written.

Left: one object holds loading true, error Timed out, and products Tea at once, marked allowed by the flags. Right: three separate cards, loading, error with a message, and success with products, marked one state at a time.Flagsrequestloading: trueerror: "Timed out"products: ["Tea"]all at once: allowedA unionstatus: "loading"status: "error", errorstatus: "success", productsone state at a time
Left, the flag type: loading, failed, and done all at once still fits. Right, the union: a request is in exactly one of three states, and each state carries only what it needs.

A union pays off again when you add a state. A switch can end with a default that stores the value in a variable of type never, the type with no values. If every state has a case, nothing reaches default, and that's fine. Add a state and watch:

Playground: add a state to the request
A message for each state of a requestPop out in the code editor (opens in a new tab)TypeScript
type ProductsRequest =  | { status: "loading" }  | { status: "error"; error: string }  | { status: "success"; products: string[] }; function message(request: ProductsRequest): string {  switch (request.status) {    case "loading":      return "Loading…";    case "error":      return `Failed: ${request.error}`;    case "success":      return `${request.products.length} products`;    default: {      const unhandled: never = request;      return unhandled;    }  }}console.log(message({ status: "success", products: ["Tea", "Cake"] }));
Console output2 products
What happens
TypeScriptNo errors
Step 1 of 3

Every state has a case. In default, request has no states left, so its type is never, and storing it in unhandled: never is fine.

TypeScript checks the code for real at every step. The last line shows a success, so every step prints the same. Reset goes back to step 1.

At step 2, the union gets an "empty" state on line 5, and line 16 is marked: Type '{ status: "empty"; }' is not assignable to type 'never'. The error names the state the switch forgot. Step 3 adds its case, and the error is gone. In a real app, this finds every switch that needs the new state, across every file. (Exhaustiveness checking shows other ways to write it.)

Small, precise types

A precise type says exactly what's allowed: "light" | "dark" rather than string. Three tools help keep types precise.

satisfies for config objects

For a settings object, you want two things: a check against Settings, and the exact values you wrote. An annotation gives only the first. satisfies gives both:

Settings checked with satisfiesPop out in the code editor (opens in a new tab)TypeScript
type Theme = "light" | "dark";type Settings = { theme: Theme; pageSize: number }; const settings = {  theme: "dark",  pageSize: 20,} satisfies Settings;const theme = settings.theme;//    ^?console.log(theme);

Lines 1 and 2 describe the settings. Lines 4 to 7 make them and check them with satisfies Settings. Line 8 reads the theme, and the query on line 9 answers const theme: "dark", the exact value. With const settings: Settings, it would be the wider Theme. The check still works, on purpose here:

A typo in a setting (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type Theme = "light" | "dark";type Settings = { theme: Theme; pageSize: number }; const settings = {  theme: "drak",  pageSize: 20,} satisfies Settings;

Line 5 spells "drak", and TypeScript marks it: Type '"drak"' is not assignable to type 'Theme'. (as const and satisfies covers both.)

readonly where a change would be a bug

A function that gets an array from someone else shouldn't change it. Say so with readonly, and TypeScript holds you to it, on purpose here:

Halving prices in the caller's array (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
function halfPrice(prices: readonly number[]) {  for (let i = 0; i < prices.length; i++) {    prices[i] = prices[i] / 2;  }  return prices;} const menu = [4, 6];console.log(halfPrice(menu), menu);

Line 1 takes prices as a readonly number[]. Line 3 writes a new price into it, and TypeScript says Index signature in type 'readonly number[]' only permits reading. The bug is real: run it, and line 9 prints the halved prices and a menu that is halved too, [2, 3]. Return a new array instead:

Halving prices in a new arrayPop out in the code editor (opens in a new tab)TypeScript
function halfPrice(prices: readonly number[]) {  return prices.map((price) => price / 2);} const menu = [4, 6];console.log(halfPrice(menu), menu);

Line 2 builds a new array with map (map, filter, and reduce). No errors, and line 6 prints [2, 3] and the menu unchanged, [4, 6]. (Arrays and tuples covers readonly arrays.)

An as const object instead of an enum

An enum is one of the few TypeScript features that adds JavaScript of its own, so tools that only remove types can't run it (Enums explains this). A plain object with as const gives you the same names, and a union type from its values:

Order statuses as a const objectPop out in the code editor (opens in a new tab)TypeScript
const OrderStatus = {  Paid: "paid",  Shipped: "shipped",} as const;type OrderStatus = (typeof OrderStatus)[keyof typeof OrderStatus];//   ^? function label(status: OrderStatus) {  return status === OrderStatus.Paid ? "Paid" : "On its way";}console.log(label("shipped"));

Lines 1 to 4 are an ordinary object, made as const so each value keeps its exact text. Line 5 builds a type from its values, and the query on line 6 answers type OrderStatus = "paid" | "shipped". Line 9 compares with OrderStatus.Paid, as an enum would. Line 11 prints On its way.

Check data where it comes in

First, turn "strict": true on in tsconfig.json. It turns on the checks this lesson relies on, like strictNullChecks (which reports values that may be null or undefined) and noImplicitAny (which asks for parameter types). Strict mode options lists them all.

Strict checks can't see inside data that arrives while the page runs: a server reply, localStorage, a form, the address bar. That data is a boundary. Check it once, there, and the rest of your code can trust it. Compare a promise with a check:

Playground: trust the server, or check it
Reading a product from the serverPop out in the code editor (opens in a new tab)TypeScript
type Product = { name: string; price: number }; function isProduct(data: unknown): data is Product {  return (    typeof data === "object" && data !== null &&    "name" in data && typeof data.name === "string" &&    "price" in data && typeof data.price === "number"  );} const text = '{"name":"Tea","price":"3"}';const product = JSON.parse(text) as Product;console.log(product.price + 1);
Console output31
What happens
TypeScriptNo errors
The reply{"name":"Tea","price":"3"}
Try it yourself

as Product is a promise TypeScript can't check. There are no errors for any reply, so a price that arrives as text joins with 1 into "31", and an empty object gives NaN.

Pick how the code reads the reply, then what the server sends. TypeScript reports no errors either way, and the output is what the code prints. Reset goes back to as Product and a price as text.

Lines 3 to 9 are isProduct, a type guard: a function whose return type data is Product tells TypeScript what a true answer means. With as Product, every reply has no errors, and a price sent as text prints 31. With isProduct check, the same reply prints Not a product, and only a real number reaches data.price + 1.

A server window sends text. An arrow leads to a card isProduct(data). From it, one arrow marked yes leads to a card data.price + 1, and one marked no leads to a card Not a product.servertextisProduct(data)yesdata.price + 1no"Not a product"the check runs while the page runs
The reply is checked once, where it comes in. Past isProduct, the code can trust the price. A reply that fails the check never reaches it.

Larger apps often use a library that builds these checks from a description of the data. Typing fetch and JSON shows both ways.

Types that stay fast to check

In a big project, the editor can slow down: errors take a while to appear, and hovering lags. Most small apps never get there. When yours does, measure first. This command prints how much work the check did:

Measure a type checkShell
npx tsc --noEmit --extendedDiagnostics

Its report includes counts like Types and Instantiations, and times like Check time. Run it before and after a change to see whether the change helped. The TypeScript team's performance guide suggests a few habits. The first one also gives better errors. Compare an intersection, &, with an interface that extends another:

Two clashing ids, combined with & (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type Product = { id: number; name: string };type CartLine = Product & { id: string; quantity: number }; const line: CartLine = { id: "7", name: "Tea", quantity: 2 };

Line 2 combines Product with a type whose id is a string, but Product's id is a number. Nothing is reported there. A value can't be both, so the combined id is never, and line 4 gets the confusing Type 'string' is not assignable to type 'never'.

The same clash, with extends (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
interface Product { id: number; name: string }interface CartLine extends Product { id: string; quantity: number } const line: CartLine = { id: "7", name: "Tea", quantity: 2 };

Now line 2 itself is marked: Interface 'CartLine' incorrectly extends interface 'Product', ending in Type 'string' is not assignable to type 'number'. The error is where the mistake is. The interface is also one flat type, which the performance guide says the compiler checks faster than an intersection. (Intersection types compares the two.)

Habits that keep checking fast. Read each row as: when it applies, what to try, and why it helps.
WhenTryWhy it helps
You combine object typesinterface B extends A instead of A & {…}The compiler builds one flat type and reports clashes where you wrote them.
An exported function returns a big typeName the type and write it as the return typeThe compiler and your editor reuse the name instead of working the type out again.
A union grows to dozens of membersA base type with the shared propertiesEach value is compared against fewer types.
Checking node_modules takes long"skipLibCheck": trueTypeScript stops checking every .d.ts file, which are usually already right.
Every check starts from scratch"incremental": trueTypeScript saves what it learned in a .tsbuildinfo file and checks only what changed.

Reviewing a checkout module

Here is a checkout screen as it might arrive in a code review. It has no type errors, and it throws when it runs:

A checkout before reviewPop out in the code editor (opens in a new tab)TypeScript
type Checkout = { paid: boolean; failed?: boolean; receipt?: string }; function show(checkout: Checkout) {  if (checkout.paid) return `Receipt ${checkout.receipt!.toUpperCase()}`;  return checkout.failed ? "Payment failed" : "Waiting";} const saved: any = JSON.parse('{"paid":true}');console.log(show(saved));

Line 1 uses flags. Line 4 reads checkout.receipt!, promising the receipt is there. Line 8 parses saved data as any. The saved checkout says it's paid but has no receipt, so line 4 throws a TypeError. Each habit from this lesson removes one way this can happen:

The same checkout after reviewPop out in the code editor (opens in a new tab)TypeScript
type Checkout =  | { status: "waiting" }  | { status: "failed" }  | { status: "paid"; receipt: string }; function show(checkout: Checkout): string {  switch (checkout.status) {    case "paid":      return `Receipt ${checkout.receipt.toUpperCase()}`;    case "failed":      return "Payment failed";    case "waiting":      return "Waiting";  }} function isCheckout(data: unknown): data is Checkout {  if (typeof data !== "object" || data === null || !("status" in data)) return false;  if (data.status === "paid") return "receipt" in data && typeof data.receipt === "string";  return data.status === "waiting" || data.status === "failed";} const saved: unknown = JSON.parse('{"status":"paid"}');console.log(isCheckout(saved) ? show(saved) : "Unknown checkout");

Lines 1 to 4 list the states, and only a paid checkout has a receipt. Line 6 writes the return type. The switch on lines 7 to 14 reads checkout.receipt with no !, because inside case "paid" TypeScript knows it exists. Lines 17 to 21 check saved data, and line 23 types it unknown. The same broken data now prints Unknown checkout instead of throwing.

The whole lesson in one table. Read each row as: what to look for, what to write instead, and the lesson that teaches it.

Habits at a glance
Instead ofWriteWhere to learn it
const items: string[] = ["Tea"]const items = ["Tea"], and types on parameters and exported return typesType annotations & inference
data: anydata: unknown, then narrow itany, unknown & never
value! or value as ProductA check: if, ??, ?., or a type guardNarrowing with typeof & truthiness; Type assertions
{ loading: boolean; error?: string }A union with a status in each memberDiscriminated unions
A switch that skips a caseA default that stores the value in a neverExhaustiveness checking
const settings: Settings = {…}const settings = {…} satisfies Settingsas const & satisfies
A parameter the function changes by accidentreadonly number[]Arrays & tuples
enum Status { … }An as const object and a union of its valuesEnums
JSON.parse(text) as Orderunknown, and a function that checks itType predicates & assertion functions; Typing fetch & JSON
"strict": false"strict": true in tsconfig.jsonStrict mode options

Now sort some lines from a review. Does each one keep TypeScript checking, or switch it off?

Keeps checking, or switches it off?
  • const data: unknown = JSON.parse(text);
  • const data: any = JSON.parse(text);
  • const count = stock.get("tea") ?? 0;
  • const count = stock.get("tea")!;
  • const user = JSON.parse(text) as User;
  • const settings = { theme: "dark" } satisfies Settings;
  • // @ts-ignore
  • function cheapest(prices: readonly number[]) {}
Try it yourself
0 of 8 correct

Put each line under what it does to TypeScript's checks.

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

Common mix-ups

  • "More annotations make code safer." No. Inside your code they repeat what TypeScript already knows, and a wide one like : string can throw away precision. Write types at the edges.
  • "as converts a value." No. It only changes what TypeScript believes. Nothing runs.
  • "! checks for null." No. It tells TypeScript not to check. If the value is missing, the code goes on with it.
  • "unknown is just a stricter any, so it doesn't matter which." It matters: any turns checks off for everything made from the value, and unknown keeps them on until you check.
  • "Optional properties are the easy way to model states." They allow every mix, including impossible ones. A union with a status allows only real states.
  • "A slow editor means you need fewer types." No. It usually means some types are built in a costly way. Measure, then use interfaces, names, and base types.

Practice exercises

Each exercise is TypeScript. Press Edit & run on the starter code to change it: the errors update as you type, and the console shows what it prints. Several starters have no errors at all. That's the point: find what the habit would catch.

Exercise 1 · Warm-upRemove the annotation that hurts

Line 7 is marked. Fix it by removing something, not adding. What type does TypeScript give size then?

Starter codePop out in the code editor (opens in a new tab)TypeScript
type Size = "small" | "large";function pour(size: Size) {  console.log(`A ${size} tea`);} const size: string = "large";pour(size);

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

    Exercise 2 · Warm-upCheck an unknown count

    Fix the error without using any or as. What does your fixed code print?

    Starter codePop out in the code editor (opens in a new tab)TypeScript
    const saved: unknown = JSON.parse("5");console.log(saved + 1);

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

      Exercise 3 · PracticeReplace the ! with a real check

      No errors here. What does the starter print? Then remove the ! and handle a missing price.

      Starter codePop out in the code editor (opens in a new tab)TypeScript
      const prices = new Map([["tea", 3], ["cake", 4]]);const scone = prices.get("scone")!;console.log(scone * 2);

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

        Exercise 4 · PracticeTurn a flag into a union

        What does the starter print? Change the type so that call becomes an error, then fix the call.

        Starter codePop out in the code editor (opens in a new tab)TypeScript
        type Visitor = { loggedIn: boolean; name?: string }; function greet(visitor: Visitor) {  return visitor.loggedIn ? `Hi, ${visitor.name}` : "Hi, guest";}console.log(greet({ loggedIn: true }));

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

          Exercise 5 · ChallengeMake the receipt switch exhaustive

          This prints undefined, with no error. Which payment method does the switch forget? Make TypeScript tell you, then handle it.

          Starter codePop out in the code editor (opens in a new tab)TypeScript
          type Payment =  | { method: "card"; last4: string }  | { method: "cash" }  | { method: "voucher"; code: string }; function receipt(payment: Payment) {  switch (payment.method) {    case "card":      return `Card ending ${payment.last4}`;    case "cash":      return "Paid in cash";  }}console.log(receipt({ method: "voucher", code: "TEA10" }));

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

            Exercise 6 · ChallengeCheck an order at the boundary

            What does the starter print? Replace the as Order with a type guard.

            Starter codePop out in the code editor (opens in a new tab)TypeScript
            type Order = { id: number; total: number }; const order = JSON.parse('{"id":7,"total":"12"}') as Order;console.log(order.total + 3);

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

              Check your understanding

              The quiz shows its code without the checker, so the error list doesn't give the answers away. For each question, ask what TypeScript checks, and what really happens when the code runs.

              The habits of good TypeScript quiz · 7 questionsScore: first tries count
              1. Question 1 of 7In a small shop app, which of these needs a type written on it?

                Choose an answer to see the explanation.

              2. Question 2 of 7What does as Product do when the code runs?

                Choose an answer to see the explanation.

              3. Question 3 of 7What does this stock code print, and does TypeScript report anything?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                const stock = new Map([["tea", 5]]);
                const cakes = stock.get("cake")!;
                console.log(cakes + 1);

                Choose an answer to see the explanation.

              4. Question 4 of 7Does TypeScript accept this request object?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type ProductsRequest = { loading: boolean; error?: string; products?: string[] };
                const request: ProductsRequest = { loading: true, error: "Timed out", products: ["Tea"] };

                Choose an answer to see the explanation.

              5. Question 5 of 7Why give the result of JSON.parse the type unknown rather than any?

                Choose an answer to see the explanation.

              6. Question 6 of 7You want TypeScript to check a settings object against Settings and keep theme as the exact type "dark". What do you write?

                Choose an answer to see the explanation.

              7. Question 7 of 7The editor is slow on a type built from many & intersections. What does the TypeScript team suggest?

                Choose an answer to see the explanation.

              Key takeaways

              • Let TypeScript work out types inside your code. Write them on parameters, exported return types, and outside data.
              • Use unknown for values you haven't checked, and narrow them. any switches checking off.
              • as and ! change what TypeScript believes, not the value. Prefer a check: if, ??, ?., or a type guard.
              • Model states as a union with a status, and end each switch with a never check.
              • Keep types precise: satisfies for config, readonly for inputs you mustn't change, and as const objects instead of enums.
              • Keep strict on, and check outside data once, where it comes in.
              • For speed, measure first, then prefer interfaces that extend, named types, and base types over huge unions.

              Remember the one-liner.
              Write types at the edges, check values instead of silencing errors, and make the types say exactly what can happen.

              Coming next: Inside the compiler. It follows your code through TypeScript's scanner, parser, binder, checker, and emitter, and shows what each one does.

              CompleteFrontend Clear concepts. Working examples.