cf.completefrontend
Open lab My progress
THE TYPESCRIPT FIELD GUIDE

Limits & soundness

See where TypeScript lets unsafe code through on purpose, why its design allows it, how to close each gap, and what to do when types get too deep.

By the end, you can
  • 01
    Name the unsound spotsSpot the places where code passes the checker and still fails: any, as, !, index reads, wider arrays, methods, and optional members.
  • 02
    Close the gaps you canSwap each hole for a checked version: unknown with a check, annotations, readonly, and stricter settings.
  • 03
    Know when to stopRead recursion and size errors, keep types simple enough to check, and check outside data while the code runs.

The checker says yes, and the code still crashes

Control flow analysis showed a call that cleared a coupon while TypeScript said nothing. That wasn't a bug in TypeScript. It is one of a few places where TypeScript lets unsafe code through on purpose. Here is a more common one:

A saved user that passes the checkerPop out in the code editor (opens in a new tab)TypeScript
type User = { name: string; cartCount: number }; const reply = '{"name":"Asha"}';const user = JSON.parse(reply) as User;console.log(`${user.name} has ${user.cartCount.toFixed(0)} items`);

Line 1 describes a User with a name and a number of cart items. Line 3 is a reply from a server, as text, and it has no cartCount. Line 4 parses it. JSON.parse returns any, a type that turns checking off, and as User tells TypeScript to treat the result as a User. TypeScript reports no errors. Line 5 reads user.cartCount, which is undefined, and toFixed throws a TypeError.

A type system is sound when every program it accepts is free of type errors when it runs: no value ever reaches a place whose type rules it out. TypeScript is not sound. That doesn't mean it's broken. It means it made a few choices, and knowing them tells you exactly where to add a check.

Two windows. Accepted holds most safe code and a few unsafe spots, marked can still crash. Rejected holds most unsafe code and some safe code, marked you rewrite it.acceptedmost safe codea few unsafe spotscan still crashrejectedmost unsafe codesome safe codeyou rewrite it
TypeScript sorts code into accepted and rejected. Most safe code is accepted and most unsafe code is rejected, but neither side is perfect. The few unsafe spots it accepts are what this lesson is about.
Real-life analogyA guard at the office door

An office has a guard who checks every badge at the front door. Most people without a badge are stopped. But the guard lets in anyone with a note from a staff member, without phoning to check. There's a side door that nobody watches. And once someone is inside, the guard never sees them again. The office is much safer with the guard, and you still lock the server room.

In real life: The guard checks every badge at the front door
In TypeScript: TypeScript checks your code before it runs
In real life: A visitor waves a note saying "I'm with them"
In TypeScript: as User tells TypeScript to trust you
In real life: A side door nobody watches
In TypeScript: A value typed any
In real life: The guard can't see what happens inside after you pass
In TypeScript: Nothing checks values while the code runs
The main idea

TypeScript is not sound, on purpose. It catches most type mistakes, lets a few known patterns through so that everyday JavaScript fits, and checks nothing while the code runs. Knowing those few spots tells you where to be careful.

Soundness vs usefulness

The TypeScript team wrote down what the language aims for, on the TypeScript Design Goals page. After the goals comes a list of non-goals: things TypeScript deliberately doesn't try to do. The third non-goal is about soundness:

From TypeScript's design goals

Non-goal 3: "Apply a sound or 'provably correct' type system. Instead, strike a balance between correctness and productivity."

In plain words: a fully sound checker would reject a lot of everyday JavaScript, and people would stop using it. TypeScript accepts some risk so that it fits the code people already write. The handbook's note on soundness says the same: "The places where TypeScript allows unsound behavior were carefully considered".

The table below lists four lines from that page. Read each row as: what the page says, what it means, and what it means for soundness.

Design goals that shape soundness
What the page saysIn plain wordsWhat it means for soundness
Goal 1: Statically identify constructs that are likely to be errors.Catch the mistakes people really make, before the code runs.Common bugs, like a typo or a missing property, are errors. Rare ones may slip through.
Goal 3: Impose no runtime overhead on emitted programs.Add nothing to the JavaScript that runs.No checks run in the browser, so data from outside is never checked by TypeScript.
Goal 7: Preserve runtime behavior of all JavaScript code.Your code does the same thing with or without types.Types can't change what the code does, so they can only report, not stop, a bad value.
Non-goal 3: Apply a sound or "provably correct" type system.Being provably right everywhere is not a goal.Some patterns are allowed even though they can fail, because rejecting them would block a lot of working code.

Goals 3 and 7 explain the last part of this lesson. Types are removed before the code runs (see How TypeScript works), so nothing checks the data your page receives.

Holes you open yourself

Some holes only open when you write them. Each one tells TypeScript to stop checking a value:

Pick a hole, then close it, and watch what TypeScript says:

Playground: open and close a hole
Code that passes or fails the checkerPop out in the code editor (opens in a new tab)TypeScript
type User = { name: string; cartCount: number }; const saved: any = JSON.parse('{"name":"Asha"}');const user: User = saved;console.log(user.cartCount.toFixed(0));
The holeany
What happens
TypeScriptNo errors
When it runsthrows a TypeError
Try it yourself

any turns checking off for saved, so line 4 stores it as a User with no question asked. The reply has no cartCount, and line 5 throws a TypeError.

TypeScript checks each version for real. Closed changes one line, or for Index, one setting. Every version still throws when it runs, because types are removed first: closing a hole makes TypeScript tell you before that. Reset goes back to any, as written.

Every version throws when it runs, closed or not: the types are removed first. Closing a hole doesn't make the code work. It makes TypeScript tell you it won't, while you write it, instead of a shopper finding out.

Holes built into the rules

Other holes need no any or as. They come from rules TypeScript chose so that common JavaScript fits. Variance showed two: lists are treated as covariant (the Push hole above), and methods are checked both ways:

A method that accepts too narrow a handlerPop out in the code editor (opens in a new tab)TypeScript
type Product = { name: string; price: number };type SaleProduct = Product & { salePrice: number }; interface ListProps { onSelect(product: Product): void } const props: ListProps = { onSelect: (item: SaleProduct) => console.log(item.salePrice.toFixed(2)) };props.onSelect({ name: "Tea", price: 3 });

Line 4 writes onSelect as a method. Line 6 gives it a handler that needs a sale price. TypeScript allows it, because method parameters are checked both ways, even with strict on. Line 7 calls it with tea, which has no salePrice, and toFixed throws. Written as a property, onSelect: (product: Product) => void, line 6 would be an error.

A narrower type can also hide a property, and an optional property can then bring it back with the wrong type:

An age that changes typePop out in the code editor (opens in a new tab)TypeScript
type Profile = { name: string; age: number };type Card = { name: string };type Contact = { name: string; age?: string }; const profile: Profile = { name: "Asha", age: 31 };const card: Card = profile;const contact: Contact = card;console.log(contact.age?.toUpperCase());
  1. Line 5 makes a profile whose age is the number 31.
  2. Line 6 stores it as a Card, which only lists name. That's allowed: extra properties are fine (see Structural typing). The type has forgotten the age, but the object hasn't.
  3. Line 7 stores the card as a Contact. Contact's age is optional, and Card says nothing about an age, so that's allowed too.
  4. Line 8 calls toUpperCase on contact.age, which is really 31. It throws contact.age?.toUpperCase is not a function.

Without the middle step, TypeScript catches it:

A profile stored as a contact (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type Profile = { name: string; age: number };type Contact = { name: string; age?: string }; const profile: Profile = { name: "Asha", age: 31 };const contact: Contact = profile;

Line 5 gets an error on purpose: Type 'Profile' is not assignable to type 'Contact', and the next line of the message gives the reason, Types of property 'age' are incompatible. The hole needs the Card step, which forgets the age. Excess property checks don't help: they only look at object literals written in place, and line 6 of the first example stores a variable.

Parameters have a hole too. A function with fewer parameters fits where more are passed. That one is safe, and you use it all the time:

A callback that takes one of three argumentsPop out in the code editor (opens in a new tab)TypeScript
const prices = [3, 4, 5];prices.forEach((price) => console.log(price));

forEach calls the callback with three arguments: the item, its index, and the list. The callback on line 2 takes only the first and ignores the rest, so it prints 3, 4, and 5. Optional parameters are where it isn't safe. The handbook calls the rule for them "unsound from a type system perspective":

An optional parameter swapped for a booleanPop out in the code editor (opens in a new tab)TypeScript
const formatPrice = (price: number, currency?: string) =>  `${price} ${currency?.toUpperCase() ?? "USD"}`; const short: (price: number) => string = formatPrice;const withSale: (price: number, onSale: boolean) => string = short;console.log(withSale(3, true));

Lines 1 and 2 make formatPrice, which takes a price and an optional currency. Line 4 stores it as a function that takes only a price: fine. Line 5 stores that as a function that takes a price and a boolean: also fine, because a function with fewer parameters fits. But the function is still formatPrice, so line 6 passes true as its currency, and currency?.toUpperCase is not a function.

Where TypeScript stays strict: Object.keys

Sometimes the choice goes the other way, and TypeScript is stricter than you'd like. Object.keys is the best-known case:

Reading a price with each key (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type Prices = { tea: number; cake: number };const prices: Prices = { tea: 3, cake: 4 }; for (const key of Object.keys(prices)) {  console.log(prices[key]);}

Line 4 loops over Object.keys(prices), and line 5 reads prices[key]. TypeScript marks line 5 on purpose: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'Prices'. In plain words: key is any string, and Prices has no entry for any string. Object.keys returns string[], not ("tea" | "cake")[], because an object can have more keys than its type lists:

A cast that adds text to a totalPop out in the code editor (opens in a new tab)TypeScript
type Prices = { tea: number; cake: number };const menu = { tea: 3, cake: 4, note: "Fresh today" };const prices: Prices = menu; let total = 0;for (const key of Object.keys(prices) as (keyof Prices)[]) {  total += prices[key];}console.log(total);

Line 2's menu has an extra note. Line 3 stores it as Prices, which is allowed: extra properties fit. Line 6 casts the keys to (keyof Prices)[], which is what people wish Object.keys returned, and TypeScript reports nothing. But the loop also visits note, so line 7 adds "Fresh today" to the total, and line 9 prints 7Fresh today.

A tag named prices, with the type Prices, points at a window named menu. The menu holds tea: 3, cake: 4, and note: Fresh today. The type lists 2 keys; Object.keys finds 3.pricesPricesthe type lists 2 keysmenutea: 3cake: 4note: Fresh todayObject.keysfinds 3 keys
prices has the type Prices, which lists two keys. The object it points at is menu, with three. Object.keys reads the real object, so it finds note too.

When you know the keys you want, list them yourself:

Looping over a list of known keysPop out in the code editor (opens in a new tab)TypeScript
type Prices = { tea: number; cake: number };const menu = { tea: 3, cake: 4, note: "Fresh today" };const prices: Prices = menu; const items = ["tea", "cake"] as const;let total = 0;for (const key of items) {  total += prices[key];}console.log(total);

Line 5 lists the keys with as const (see const assertions and satisfies), so key is "tea" | "cake". Lines 7 and 8 add only those two prices, and line 10 prints 7. Here TypeScript's strictness was right, and the cast was the hole.

How deep a type can go

A recursive type refers to itself, the way a recursive function calls itself. Writing utility types showed how to write them. The checker has to stop somewhere, or a type that never ends would freeze your editor:

A tuple of any length, until it's too long (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type Tuple<Item, Length extends number, Items extends Item[] = []> =  Items["length"] extends Length ? Items : Tuple<Item, Length, [...Items, Item]>; type Week = Tuple<number, 7>;//   ^?type Huge = Tuple<number, 1000>;

Lines 1 and 2 describe Tuple: if the list Items already has Length items, that's the answer; if not, call Tuple again with one more item. Line 4 asks for 7 numbers, like a week of daily sales, and the query on line 5 shows [number, number, number, number, number, number, number]. Line 6 asks for 1,000, and TypeScript stops with an error, on purpose: Type instantiation is excessively deep and possibly infinite (error 2589). "Instantiation" means filling in a generic type's parameters. In TypeScript 5.9 this type works up to 999 items and fails at 1,000.

Tuple is tail-recursive: when it calls itself, that call is the whole answer of the branch, with nothing added around it. Since TypeScript 4.5, the checker handles such types without keeping every step, so they can go much deeper. The same type written so that each step adds to the next step's result fails far sooner:

The same tuple, not tail-recursive (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type Tuple<Item, Length extends number, Count extends unknown[] = []> =  Count["length"] extends Length ? [] : [Item, ...Tuple<Item, Length, [...Count, unknown]>]; type Week = Tuple<number, 7>;type Row = Tuple<number, 60>;

Line 2 now puts an Item in front of what the next call gives back: [Item, ...Tuple<…>]. Line 4 still works, but line 5 asks for only 60 items and gets the same error 2589.

A different limit is about size:

A union that's too big (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type Digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"; type Pin = `${Digit}${Digit}${Digit}${Digit}`;type LongPin = `${Digit}${Digit}${Digit}${Digit}${Digit}`;

Line 1 lists the ten digits as text. Line 3's Pin is every 4-digit text, a union of 10,000 members, and that's fine. Line 4 would need 100,000, and TypeScript marks it on purpose: Expression produces a union type that is too complex to represent (error 2590).

Neither limit is a setting you can raise. When you hit one, it's usually time to stop making the type fancier:

  • Prefer a plainer type and a check while the code runs: number[] and a length check, not a 1,000-item tuple.
  • If you need recursion, end each branch with the call itself, and carry the result in an extra type parameter, as Tuple carries Items.
  • Ask what the type catches that a simpler type and a test wouldn't. Deep types also slow down every check in your editor. Habits of good TypeScript covers type performance.

Runtime checks still matter

Every hole so far ends the same way: a value that doesn't match its type, found while the code runs. The design goals explain why TypeScript can't help then: the types are gone. Anything that arrives from outside, like a server reply, localStorage, or the URL, is unchecked. Check it yourself, at the edge, where it enters your code:

A guard at the edgePop out in the code editor (opens in a new tab)TypeScript
type User = { name: string; cartCount: number }; function isUser(value: unknown): value is User {  return (    typeof value === "object" && value !== null &&    "name" in value && typeof value.name === "string" &&    "cartCount" in value && typeof value.cartCount === "number"  );} const saved: unknown = JSON.parse('{"name":"Asha"}');if (isUser(saved)) {  console.log(`${saved.name} has ${saved.cartCount} items`);} else {  console.log("The saved user is out of date");}

Line 3's isUser is a type guard: it takes unknown and returns value is User. Lines 5 to 7 check that the value is an object with a text name and a number cartCount. Line 11 parses the saved user as unknown, so nothing can use it unchecked. Line 12 runs the check. This saved user has no cartCount, so line 15 prints The saved user is out of date instead of crashing. Inside line 13, saved is a User.

A server window sends a card, name: Asha, with no cartCount. An arrow leads to a card isUser(saved), checked while the code runs. An arrow leads to an app window that shows out of date, marked no crash.servername: Ashano cartCountisUser(saved)checked whilethe code runsappout of dateno crash
The reply arrives while the code runs, when the types are gone. isUser checks it at the edge, so the app shows a message instead of crashing on the missing cartCount.

Writing guards by hand gets long for big replies. Typing fetch and JSON shows more ways to check a server's reply.

Auditing a settings loader

This project turns on noUncheckedIndexedAccess. Here is a settings loader that still passes the checker with three holes in it:

A settings loader that passes the checkerPop out in the code editor (opens in a new tab)TypeScript
type Settings = { theme: "light" | "dark"; fontSizes: number[] }; function loadSettings(text: string) {  const settings = JSON.parse(text) as Settings;  const largest = settings.fontSizes[settings.fontSizes.length]!;  return `${settings.theme}, ${largest.toFixed(0)}px`;} console.log(loadSettings('{"theme":"dark","fontSizes":[14,16]}'));
  1. Line 4: as Settings trusts whatever the text holds.
  2. Line 5 reads index length, one past the end: the last item is at length - 1. The setting would mark it as possibly undefined, but the ! turns that check off.
  3. Line 6 calls toFixed on undefined, and line 9's call throws a TypeError.
The same loader with a guard and no holesPop out in the code editor (opens in a new tab)TypeScript
type Settings = { theme: "light" | "dark"; fontSizes: number[] }; function isSettings(value: unknown): value is Settings {  return (    typeof value === "object" && value !== null &&    "theme" in value && (value.theme === "light" || value.theme === "dark") &&    "fontSizes" in value && Array.isArray(value.fontSizes) &&    value.fontSizes.every((size) => typeof size === "number")  );} function loadSettings(text: string) {  const settings: unknown = JSON.parse(text);  if (!isSettings(settings)) return "default settings";  const largest = settings.fontSizes.at(-1);  if (largest === undefined) return `${settings.theme}, no sizes`;  return `${settings.theme}, ${largest.toFixed(0)}px`;} console.log(loadSettings('{"theme":"dark","fontSizes":[14,16]}'));console.log(loadSettings('{"theme":"blue"}'));
  1. Lines 3 to 10 are a guard: the theme must be "light" or "dark", and fontSizes must be a list of numbers.
  2. Line 13 parses the text as unknown, and line 14 falls back to the defaults when the guard says no.
  3. Line 15's .at(-1) gives the last size, or undefined for an empty list, and line 16 handles that.

Line 20 prints dark, 16px. Line 21 prints default settings, because "blue" isn't a theme.

Caught or slips through?
  • const count: number = JSON.parse('"three"');
  • const saved: unknown = JSON.parse('"three"'); const count: number = saved;
  • const user = {} as { name: string };
  • const user: { name: string } = {};
  • const sizes: string[] = []; const first: string = sizes[0];
  • const keys: ("tea" | "cake")[] = Object.keys({ tea: 3, cake: 4 });
  • const teas: string[] = ["green"]; const drinks: (string | number)[] = teas; drinks.push(3);
  • type T<N extends unknown[]> = N["length"] extends 1000 ? N : T<[...N, 0]>; type Big = T<[]>;
Try it yourself
0 of 8 correct

Every line here is unsafe. Sort each one by what TypeScript says about it, with strict on. Each line 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

The table below sums up the holes in this lesson. Read each row as: the hole, what slips through it, and how to close it.

Holes and how to close them
The holeWhat slips throughHow to close it
anyAny use of the value, with no checks at all.Use unknown and check the value before you use it.
as TypeA value that doesn't match the type you claim.Annotate with : Type, or check the value with a type guard.
value!null or undefined where the type says there's none.Check for the missing value, or use ?. and ??.
items[i]undefined when the index is past the end.Turn on noUncheckedIndexedAccess, or use .at().
A list or object passed as a wider typeA write through the wider name that breaks the original.Use readonly where you only read.
A method in an interfaceA callback that needs more than the caller gives it.Write callbacks as properties with function types.
An optional propertyA value of another type that was hidden by a narrower type.Copy only what you need into a new object.
A function with optional parametersArguments of the wrong type in those positions.Keep callback types exact, and avoid optional parameters in callbacks.
  • "No type errors means no type problems when the code runs." No. TypeScript is not sound. Values from any, as, index reads, and outside data can still be the wrong type.
  • "Every unsound spot is a bug in TypeScript." No. Most are choices, made so that everyday JavaScript fits. The design goals say soundness is not a goal.
  • "as turns a value into another type." No. It changes only what TypeScript believes. Nothing happens when the code runs.
  • "strict closes every hole." No. noUncheckedIndexedAccess is a separate setting, methods are still checked both ways, and any, as, and ! still work.
  • "Object.keys returning string[] is a mistake." No. An object can have more keys than its type lists, so string[] is the true answer.
  • "A fancier type is always safer." No. Deep types hit the checker's limits and slow your editor. A simple type and a runtime check often catch more.

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. Find where checking stopped, then use the hints.

Exercise 1 · Warm-upPredict what an assertion lets through

TypeScript reports no errors. What does this code print? Then make it check the value instead of trusting it.

Starter codePop out in the code editor (opens in a new tab)TypeScript
type Settings = { theme: string; fontSize: number }; const settings = JSON.parse('{"theme":"dark"}') as Settings;console.log(settings.fontSize + 2);

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

    Exercise 2 · Warm-upFind the line the setting catches

    This project turns on noUncheckedIndexedAccess. Which line will TypeScript mark?

    Starter codePop out in the code editor (opens in a new tab)TypeScript
    const sizes = ["S", "M", "L"];
    const first = sizes[0];
    const label = first.toLowerCase();
    console.log(label);

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

      Exercise 3 · PracticeReplace any with unknown

      The second call throws. Make the parameter safe so it returns 0 for a reply with no items. What does it print?

      Starter codePop out in the code editor (opens in a new tab)TypeScript
      function cartCount(saved: any) {  return saved.items.length;} console.log(cartCount(JSON.parse('{"items":[1,2]}')));console.log(cartCount(JSON.parse("{}")));

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

        Exercise 4 · PracticeAdd up prices without a cast

        TypeScript marks line 6. Fix it without as. What does it print?

        Starter codePop out in the code editor (opens in a new tab)TypeScript
        type Prices = { tea: number; cake: number };const prices: Prices = { tea: 3, cake: 4 }; let total = 0;for (const key of Object.keys(prices)) {  total += prices[key];}console.log(total);

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

          Exercise 5 · ChallengePredict the age's real type

          TypeScript says contact.age is string | undefined. What does the starter print? Then change line 6 so the hole closes.

          Starter codePop out in the code editor (opens in a new tab)TypeScript
          type Profile = { name: string; age: number };type Card = { name: string };type Contact = { name: string; age?: string }; const profile: Profile = { name: "Asha", age: 31 };const card: Card = profile;const contact: Contact = card;console.log(typeof contact.age);

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

            Exercise 6 · ChallengeMake a row of 60 seats fit

            TypeScript marks line 6 with error 2589. Rewrite Row so it's tail-recursive, and add a query under SeatCount. What does it show?

            Starter codePop out in the code editor (opens in a new tab)TypeScript
            type Seat = { row: string; seat: number }; type Row<Length extends number, Count extends unknown[] = []> =  Count["length"] extends Length ? [] : [Seat, ...Row<Length, [...Count, unknown]>]; type BigRow = Row<60>;type SeatCount = BigRow["length"];

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

              Check your understanding

              For each question, ask where the value comes from and whether anything checked it. The quiz shows its code without the checker, so the error list doesn't give the answers away.

              The limits and soundness quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What would it mean for a type system to be sound?

                Choose an answer to see the explanation.

              2. Question 2 of 7What do TypeScript's design goals say about soundness?

                Choose an answer to see the explanation.

              3. Question 3 of 7TypeScript reports no errors for this code. What does it print?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                const quantities = ["1", "2", "3"].map(parseInt);
                console.log(quantities);

                Choose an answer to see the explanation.

              4. Question 4 of 7With the default strict settings, what happens with this price list?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                const prices = [3, 4, 5];
                const fourth = prices[3];
                console.log(fourth.toFixed(2));

                Choose an answer to see the explanation.

              5. Question 5 of 7Why does Object.keys(prices) give string[] and not ("tea" | "cake")[]?

                Choose an answer to see the explanation.

              6. Question 6 of 7TypeScript reports no errors for this handler code. What does it print?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type Handler = (price: number, onSale: boolean) => string;
                
                const label = (price: number, currency?: string) => `${price} ${currency ?? "USD"}`;
                const short: (price: number) => string = label;
                const handler: Handler = short;
                console.log(handler(3, true));

                Choose an answer to see the explanation.

              7. Question 7 of 7A helper type shows Type instantiation is excessively deep and possibly infinite. What is usually the best first step?

                Choose an answer to see the explanation.

              Key takeaways

              • A sound checker never accepts code with a type error. TypeScript is not sound, on purpose: its design goals trade some correctness for productivity.
              • any, as, !, index reads, and lists passed as wider lists stop checks. unknown, annotations, noUncheckedIndexedAccess, and readonly close them.
              • Methods, hidden optional properties, and optional parameters let some unsafe code through with no cast at all.
              • Object.keys gives string[] because objects can have more keys than their types list.
              • Recursive types stop at a limit (error 2589), and huge unions too (error 2590). A simpler type is usually the answer.
              • Types are gone when the code runs. Check outside data at the edge, with a guard.

              Remember the one-liner.
              TypeScript checks your code, not your data, and it lets a few known patterns through on purpose.

              This is the last lesson of the TypeScript track. Where to go next:

              Coming next: you've finished the track. Go back to the TypeScript track to review any stage.

              CompleteFrontend Clear concepts. Working examples.