cf.completefrontend
Open lab My progress
THE TYPESCRIPT FIELD GUIDE

Your first typed program

Turn a JavaScript shopping cart into TypeScript: annotate variables and parameters, read each compiler error line by line, and fix bugs instead of hiding them.

By the end, you can
  • 01
    Annotate valuesWrite a type after a variable or a parameter, like let total: number = 0, and say what it promises.
  • 02
    Read an error messageFind the file, line, and column, read the message in plain words, and follow a message that runs over several lines.
  • 03
    Fix instead of hidingFix the code behind an error, know what @ts-ignore and @ts-expect-error hide, and turn a .js file into a .ts file step by step.

A cart that prints $NaN

Here is a small shopping cart in plain JavaScript, saved as cart.js. Two teas at 3 and one cake at 4 should cost 10. Run it and see what it prints:

cart.js, with a bug somewherePop out in the code editor (opens in a new tab)JavaScript
function lineTotal(price, quantity) {
  return price * quantity;
}

function cartTotal(items) {
  let total = 0;
  for (const item of items) {
    total += lineTotal(item.price, item.quantity);
  }
  return total;
}

const cart = [
  { name: "Tea", price: 3, quantity: 2 },
  { name: "Cake", price: 4, qty: 1 },
];
console.log(`Total: $${cartTotal(cart)}`);

Lines 1 to 3 declare lineTotal, which multiplies a price by a quantity. Lines 5 to 11 declare cartTotal, which adds up the line total of every item. Lines 13 to 16 make a cart of tea and cake, and line 17 prints the total. It prints Total: $NaN. NaN means "not a number" (the Numbers lesson explains where it comes from).

The bug is on line 15: the cake says qty, but line 8 reads item.quantity. The cake has no quantity, so line 8 gets undefined, and 4 * undefined is NaN. JavaScript doesn't warn you. Here is the same cart turned into TypeScript, with a few types added:

The same cart as TypeScript (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type CartItem = { name: string; price: number; quantity: number }; function lineTotal(price: number, quantity: number) {  return price * quantity;} function cartTotal(items: CartItem[]) {  let total = 0;  for (const item of items) {    total += lineTotal(item.price, item.quantity);  }  return total;} const cart: CartItem[] = [  { name: "Tea", price: 3, quantity: 2 },  { name: "Cake", price: 4, qty: 1 },];console.log(`Total: $${cartTotal(cart)}`);

Line 1 is new. It's a type alias: it gives the name CartItem to the shape every item must have, a name that is text and a price and quantity that are numbers. Lines 3, 7, and 15 use types: price: number and quantity: number, items: CartItem[] (a list of cart items), and cart: CartItem[]. The rest is the JavaScript you just read.

TypeScript marks line 17, on purpose: it's the bug. Object literal may only specify known properties, and 'qty' does not exist in type 'CartItem' means "this object has a property, qty, that a CartItem doesn't have". An object literal is an object written out in curly braces, like the cake. The code still prints Total: $NaN if you run it, but you found the bug before anyone did.

Left, a window cart.js holds the cake card with qty: 1, and the result Total: $NaN, found when it runs. Right, a window cart.ts holds the same card, marked: no qty in CartItem, found before it runs.cart.jsnodeCake, price: 4, qty: 1Total: $NaNwrong total, no warningcart.tseditorCake, price: 4, qty: 1no qty in CartIteman error on line 17marked before it runs
The same cake, written with qty. As JavaScript, the cart prints Total: $NaN. As TypeScript, with a CartItem type, the editor marks qty before the code runs.

This lesson builds that TypeScript version one step at a time. You'll write types, read what the compiler says about them, and fix what it finds, without hiding anything.

Real-life analogyLabels on kitchen jars

In a busy kitchen, every jar has a label. When salt ends up in the jar marked "sugar", the label is how you notice, before the cake is ruined. Tearing the label off would stop the warning, but the cake would still taste of salt.

In real life: A label on a jar says "sugar"
In TypeScript: An annotation says price: number
In real life: Someone pours salt into the sugar jar
In TypeScript: The code puts text where a number goes
In real life: You read the label and spot the mix-up before you bake
In TypeScript: TypeScript reports the line before the code runs
In real life: Peeling the label off doesn't take the salt out
In TypeScript: // @ts-ignore hides the error, and the bug stays
The main idea

Typing a program means writing down what each value must be, like price: number. TypeScript then reports every place the code breaks that promise. Read each error's line and message, and fix the code, rather than hiding the error.

Annotating a variable

A type annotation is a colon and a type, written after a name. Read let itemCount: number = 2 as "itemCount is a number, and it starts as 2". The three types you'll use most are number, string for text, and boolean for true or false.

Two numbers, one annotatedPop out in the code editor (opens in a new tab)TypeScript
let itemCount: number = 2;//  ^?let total = 0;//  ^?console.log(itemCount, total);

Line 1 annotates itemCount as a number. The query on line 2 shows let itemCount: number. Line 3 has no annotation, yet the query on line 4 shows let total: number: TypeScript worked it out from the 0. Line 5 prints 2 0. You don't have to annotate every variable. The Type annotations & inference lesson, two lessons on, shows when you can leave it out. When you do write one, it's a promise that TypeScript holds every later line to:

An annotation guards every later line (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
let total: number = 0;total = total + 3;total = "10";console.log(total);

Line 1 promises that total is a number. Line 2 adds 3, which keeps that promise. Line 3 stores the text "10", and TypeScript marks it on purpose: Type 'string' is not assignable to type 'number', meaning "you're putting text where a number has to go". Line 4 would print 10, the text. Now try annotations yourself: type a different type on line 1.

Playground: type the annotation
An item count with a typePop out in the code editor (opens in a new tab)TypeScript
let itemCount: number = 2;itemCount = itemCount + 1;console.log(itemCount);
The annotation: number
What happens
TypeScriptNo errors
When it runsprints 3
Try it yourself

itemCount holds 2, then 3. Both are numbers, so there are no errors, and line 3 prints 3.

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

With number there are no errors. string and boolean each break the promise on line 1, and boolean breaks line 2 as well. A typo like nmber gets a suggestion, and an empty annotation gets Type expected. Notice that line 3 prints 3 every time the code can run: an annotation never changes a value.

Annotations matter most on parameters, the names a function receives its values in (see Parameters). TypeScript can work out a variable's type from its starting value, but a parameter has no value until someone calls the function:

A function with typed parametersPop out in the code editor (opens in a new tab)TypeScript
function lineTotal(price: number, quantity: number) {  return price * quantity;} console.log(lineTotal(3, 2));

Line 1 says lineTotal takes a price and a quantity, both numbers. Line 2 multiplies them. Line 5 calls it with 3 and 2 and prints 6. Every call is now checked against line 1, and the next section shows what that looks like.

Reading an error message

Here is lineTotal again, called with a price that came from a form:

A price from a form (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
function lineTotal(price: number, quantity: number) {  return price * quantity;} const priceText = "4"; // read from a form, so it's textconsole.log(lineTotal(priceText, 2));

Lines 1 to 3 are the same function. Line 5 stores "4", which is text, because a form gives you text. Line 6 passes it to lineTotal, and TypeScript marks line 6 on purpose. Saved as src/cart.ts and checked with npx tsc --noEmit, this is what the terminal shows:

What tsc prints for itText
src/cart.ts:6:23 - error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

6 console.log(lineTotal(priceText, 2));
                        ~~~~~~~~~


Found 1 error in src/cart.ts:6

Every error has the same parts. Read them in this order:

  1. Where: src/cart.ts:6:23 is the file, line 6, and column 23. A column counts characters from the start of the line, so 23 is where priceText starts.
  2. Its number: TS2345. Every kind of error has one. Search for it when a message is new to you.
  3. What's wrong: the message itself, explained below.
  4. The spot: line 6 again, with ~~~~~~~~~ under the exact part the message is about. Your editor draws the same thing as a red squiggle.
  5. The count: Found 1 error, and where it is.
The first line of a tsc error split into three cards: src/cart.ts:6:23, labelled where it is; error TS2345, labelled its number; and Argument of type 'string'…, labelled what's wrong. Below, the code line console.log(lineTotal(priceText, 2)) with an arrow marked column 23 to a card priceText, labelled the squiggle marks it.src/cart.ts:6:23where it iserror TS2345its numberArgument of type 'string' …what's wrongconsole.log(lineTotal(priceText, 2));column 23priceTextthe squiggle marks it
Read the first line left to right: where the error is (file, line 6, column 23), its number, and the message. The ~~~~~~~~~ under the code marks the exact spot: priceText.

Now the message: Argument of type 'string' is not assignable to parameter of type 'number'. Take it a few words at a time:

  • An argument is a value you pass in a call: priceText.
  • A parameter is the name that receives it: price, on line 1.
  • "Not assignable to" means "not allowed in".

So: "you passed text to a parameter that takes a number". Messages like this give the type you have first and the type you need second. Pick a different mistake below and read each message the same way.

Playground: read what tsc prints
function lineTotal(price: number, quantity: number) {  return price * quantity;} const total = lineTotal("4", 2);console.log(total);
Error numberTS2345
What npx tsc --noEmit printsText
src/cart.ts:5:25 - error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

5 const total = lineTotal("4", 2);
                          ~~~
The first line, in parts
Filesrc/cart.ts
Line and columnline 5, column 25
When it runs8
Try it yourself

Where: line 5, column 25, the "4". What: an argument (a value you pass) of type string went to a parameter (the name that receives it) of type number. Run anyway, it prints 8 only because * turns "4" into a number.

The terminal text is what the real tsc 5.9 prints for this code saved as src/cart.ts, without its last line, the error count. Each button changes only line 5. Reset goes back to Text for a number.

Some messages run over several lines, with an indented part. In A missing argument, the indented part points back at line 1 and names the parameter you forgot. In A misspelled name, it shows where the right name was declared. The first line always says what went wrong; the rest adds detail. A longer message works the same way:

A scone with its price as text (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
type CartItem = { name: string; price: number; quantity: number }; const scone = { name: "Scone", price: "2", quantity: 1 };const cart: CartItem[] = [scone];

Line 1 describes a cart item. Line 3 makes a scone whose price is the text "2". Line 4 puts it in a cart of CartItems, and TypeScript marks line 4 on purpose with this message:

The whole message for line 4Text
Type '{ name: string; price: string; quantity: number; }' is not assignable to type 'CartItem'.
  Types of property 'price' are incompatible.
    Type 'string' is not assignable to type 'number'.

Read it from the top. Line 1 says the whole scone doesn't fit a CartItem. Each indented line goes one step closer: line 2 says the problem is the price property, and line 3 says why: text where a number goes. The last line is usually the one to fix. Here, write price: 2 without quotes.

The table below collects the messages from this lesson. Read each row as: the message, what it means, and the usual fix.

Common messages in plain words
The messageIn plain wordsThe usual fix
Type 'string' is not assignable to type 'number'.You put text where a number must go.Change the value, or convert it with Number(…).
Argument of type 'string' is not assignable to parameter of type 'number'.You passed text to a function that takes a number.Convert the value you pass: lineTotal(Number(text), 2).
Expected 2 arguments, but got 1.The call is missing a value.Pass every value the function asks for.
Cannot find name 'lineTotl'. Did you mean 'lineTotal'?Nothing called lineTotl exists.Fix the spelling. The message often suggests the name.
Parameter 'price' implicitly has an 'any' type.Nothing says what price holds.Write its type: price: number.

Fixing an error, or hiding it

There are two ways to make an error go away. You can fix it: change the code so the types are right. Or you can hide it, also called silencing: tell TypeScript to stop reporting it. Both empty the error list. Only one changes what the code does. Try each on the same mistake:

Playground: fix the error or hide it
Adding a price from a formPop out in the code editor (opens in a new tab)TypeScript
function addToTotal(total: number, price: number) {  return total + price;} const cakePrice = "4"; // typed into a form, so it's textconsole.log(addToTotal(3, cakePrice));
Console output34
What happens
TypeScript1 error: line 6
The bugstill there
Try it yourself

The error stays on line 6: Argument of type 'string' is not assignable to parameter of type 'number'. Run anyway, 3 + "4" joins into 34. The error is right: this is a real bug.

TypeScript checks the code for real, and the output is what this code prints. Reset goes back to Leave it.

Leave it shows the problem: 3 + "4" joins text and prints 34. Fix converts the text with Number("4"), and the code prints 7. The two Hide buttons add a comment on line 6, just above the call (which moves down to line 7). The error vanishes, but the code still prints 34. The last button shows why @ts-expect-error is the better of the two comments: once the line is fixed, it reports Unused '@ts-expect-error' directive, so a hiding comment doesn't stay after its reason is gone.

A card cakePrice = "4" at the top branches two ways. Left, fix: a card Number("4"), marked error gone, prints 7. Right, hide: a card // @ts-ignore, marked error hidden, prints 34.cakePrice = "4"fixhideNumber("4")error goneprints 7// @ts-ignoreerror hiddenprints 34
Two ways to make the same error disappear. A fix changes the value, so the code prints 7. A hiding comment only stops the message, and the code still prints 34.

// @ts-ignore tells TypeScript to ignore every error on the next line, now and forever. // @ts-expect-error says "the next line has an error, and I know". Hiding is right only when you're sure the code is correct and TypeScript can't see it, which is rare in your own code. When you do, use @ts-expect-error and write why on the same line. Two more ways to hide errors come later: the type any, in any, unknown & never, and turning strict off, which you saw in Setting up a project.

The table below compares the four ways. Read each row as: what you do, whether the bug is gone, and what TypeScript says afterwards.

Four ways to make an error go away
What you doIs the bug gone?What TypeScript says afterwards
Fix the code, like Number("4")Yes.Nothing. There's nothing left to report.
Put // @ts-ignore above the lineNo.Nothing for that line, ever, even after a new mistake.
Put // @ts-expect-error above the lineNo.Unused '@ts-expect-error' directive, once the line has no error.
Turn strict off in tsconfig.jsonNo.Nothing, for a whole group of checks in every file.

From cart.js to cart.ts

Now put it together on the cart from the start of the lesson. Turning a JavaScript file into TypeScript takes four small steps, and after each one you read the errors again. Step through them:

Playground: turn cart.js into cart.ts
function lineTotal(price, quantity) {  return price * quantity;} function cartTotal(items) {  let total = 0;  for (const item of items) {    total += lineTotal(item.price, item.quantity);  }  return total;} const cart = [  { name: "Tea", price: 3, quantity: 2 },  { name: "Cake", price: 4, qty: 1 },];console.log(`Total: $${cartTotal(cart)}`);
Console outputTotal: $NaN
What happens
TypeScript3 errors: lines 1, 1 and 5
Step 1 of 4

The code is the same; only the file name changed. With strict on, TypeScript marks three parameters it knows nothing about: Parameter 'price' implicitly has an 'any' type, then quantity on line 1 and items on line 5. They're a to-do list. The code still prints Total: $NaN.

TypeScript checks the code for real at every step, and the output is what the code prints. Reset goes back to step 1.

Step 1 only renames the file. Almost all JavaScript is already TypeScript, so the code is unchanged, and the errors are about what TypeScript can't know yet: three parameters with no type. Step 2 types price and quantity. Step 3 describes a cart item with type CartItem, and that's when the real bug shows up, on line 17. Step 4 fixes it, and the total is right.

Four cards in a row joined by arrows: rename, 3 errors; type numbers, 1 error; add CartItem, 1 error: qty; fix qty, 0 errors. The first three print $NaN and the last prints $10.rename3 errors$NaNtype numbers1 error$NaNadd CartItem1 error: qty$NaNfix qty0 errors$10
Each step changes a little and reads the errors again. The count goes from 3 to 1, then the CartItem type finds the real bug, and the fix makes the total right.

A few habits make this easier on bigger files:

  • Start with function parameters. They're where TypeScript knows least, and every call gets checked once they're typed.
  • Treat the errors as a to-do list, top to bottom. Read each message before you change anything.
  • Expect real bugs. Step 3's error is the reason the cart printed $NaN.
  • Not ready to rename? A // @ts-check comment at the top of a .js file asks your editor to check it as it is (the Types for JavaScript lesson covers this).

A price label on a product form

On a shop's admin page, the owner types a new price into a box, and the page shows a label like $4.50. Here is the JavaScript:

A price label in JavaScriptPop out in the code editor (opens in a new tab)JavaScript
const priceInput = document.createElement("input");
priceInput.value = "4.5";

function priceLabel(price) {
  return `$${price.toFixed(2)}`;
}

console.log(priceLabel(priceInput.value));

Line 1 makes the price box, and line 2 puts "4.5" in it, as if the owner typed it (Form elements covers .value). Lines 4 to 6 build the label: toFixed(2) writes a number with two decimals. Line 8 passes the box's value. It crashes: TypeError: price.toFixed is not a function, because .value is always text, and text has no toFixed. Add one annotation, on line 4:

The same label in TypeScript (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
const priceInput = document.createElement("input");priceInput.value = "4.5"; function priceLabel(price: number) {  return `$${price.toFixed(2)}`;} console.log(priceLabel(priceInput.value));

price: number on line 4 is the only change, and TypeScript marks line 8 on purpose: Argument of type 'string' is not assignable to parameter of type 'number'. The crash is now an error you see while you type. Fix it where the text comes in:

The label, fixedPop out in the code editor (opens in a new tab)TypeScript
const priceInput = document.createElement("input");priceInput.value = "4.5"; function priceLabel(price: number) {  return `$${price.toFixed(2)}`;} console.log(priceLabel(Number(priceInput.value)));

Line 8 now passes Number(priceInput.value), the number 4.5. No errors, and it prints $4.50. Each change you can make to an error either fixes the bug or only hides the message. Sort these:

Fix or hide?
  • Wrap the text in Number(…)
  • Add // @ts-ignore above the line
  • Write price: number on a parameter with no type
  • Add // @ts-expect-error above the line
  • Change qty to quantity in the cake
  • Set "strict": false in tsconfig.json
  • Check if (cake) before reading cake.price
  • Write const cakePrice: any = "4";
Try it yourself
0 of 8 correct

Sort each change by what it does about the error.

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

Common mix-ups

  • "An annotation converts the value." No. let total: number = "10" is an error, not the number 10. Number("10") converts.
  • "@ts-ignore fixes the error." No. It hides the message. The fix-or-hide playground still printed 34.
  • "I have to understand the whole long message." No. The first line says what's wrong, and the last indented line usually says which part to fix.
  • "Renaming .js to .ts changes what my code does." No. The code runs the same. The errors that appear are a to-do list, and some of them are real bugs.
  • "The error number tells me the line." No. In src/cart.ts:6:23 - error TS2345, 6 is the line and 23 the column. 2345 is the kind of error.
  • "Turning strict off is a quick way to finish converting." No. It hides a whole group of checks in every file, like the ones that asked for parameter types in step 1 of the conversion.

Practice exercises

Press Edit & run on any 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-upFind the line and column

tsc printed this line. Which line and column of the file is the error on?

Starter codeText
src/checkout.ts:7:15 - error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

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

    Exercise 2 · Warm-upFind the broken promise

    Which line will TypeScript flag?

    Starter codePop out in the code editor (opens in a new tab)TypeScript
    let itemCount: number = 0;
    itemCount = itemCount + 2;
    itemCount = "3";
    console.log(itemCount);

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

      Exercise 3 · PracticeFix the argument

      Shipping costs 4 plus the parcel's weight. Fix line 6 without hiding the error. What does the fixed code print?

      Starter codePop out in the code editor (opens in a new tab)TypeScript
      function shippingCost(weight: number) {  return weight + 4;} const weightText = "5"; // from a formconsole.log(shippingCost(weightText));

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

        Exercise 4 · PracticeFollow an error chain

        TypeScript marks line 4 with a message over three lines. Which property is wrong? Press Edit & run to read the whole message.

        Starter codePop out in the code editor (opens in a new tab)TypeScript
        type Profile = { name: string; age: number; newsletter: boolean };
        
        const asha = { name: "Asha", age: 30, newsletter: "yes" };
        const profile: Profile = asha;

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

          Exercise 5 · ChallengeReplace a hiding comment with a fix

          A bill of 10 plus a tip of 2. The starter has no errors, but it prints the wrong bill. Remove the hiding comment, fix the code, and say what it prints.

          Starter codePop out in the code editor (opens in a new tab)TypeScript
          function addTip(bill: number, tip: number) {  return bill + tip;} const tipText = "2"; // from a form// @ts-ignoreconsole.log(addTip(10, tipText));

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

            Exercise 6 · ChallengeConvert a script to TypeScript

            This was discount.js; now it's discount.ts, with strict on. Fix all three errors without hiding any. What does it print?

            Starter codePop out in the code editor (opens in a new tab)TypeScript
            function discounted(total, percent) {  return total - (total * percent) / 100;} const order = { total: 50, coupon: 10 };console.log(discounted(order.total, order.coupn));

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

              Check your understanding

              For each question, read the code or the message part by part: where it is, what's wrong, and whether a change fixes the bug or only hides it. The quiz shows its code without the checker, so the error list doesn't give the answers away.

              The your first typed program quiz · 7 questionsScore: first tries count
              1. Question 1 of 7In let total: number = 0;, what does : number do?

                Choose an answer to see the explanation.

              2. Question 2 of 7Which line of src/cart.ts does this error point at?

                Read the code, then predictText
                src/cart.ts:12:9 - error TS2322: Type 'string' is not assignable to type 'number'.

                Choose an answer to see the explanation.

              3. Question 3 of 7What does TypeScript say about line 5 of this code?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                function lineTotal(price: number, quantity: number) {
                  return price * quantity;
                }
                
                console.log(lineTotal("4", 2));

                Choose an answer to see the explanation.

              4. Question 4 of 7This code hides its error with @ts-ignore. What does it print?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                function addToTotal(total: number, price: number) {
                  return total + price;
                }
                
                const teaPrice = "3";
                // @ts-ignore
                console.log(addToTotal(4, teaPrice));

                Choose an answer to see the explanation.

              5. Question 5 of 7Which comment speaks up once the line below it has no error left?

                Choose an answer to see the explanation.

              6. Question 6 of 7Which property does this error chain point to?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                type Order = { item: string; quantity: number; paid: boolean };
                
                const draft = { item: "Tea", quantity: "2", paid: false };
                const order: Order = draft;

                Choose an answer to see the explanation.

              7. Question 7 of 7You rename cart.js to cart.ts and see Parameter 'price' implicitly has an 'any' type. What's the best next step?

                Choose an answer to see the explanation.

              Key takeaways

              • A type annotation is a colon and a type after a name, like let total: number = 0. It's a promise every later line is checked against.
              • Annotations matter most on function parameters, which have no value for TypeScript to learn from.
              • An error's first line gives the file, line, and column, the error's number, and the message: the type you have, then the type you need.
              • In a message over several lines, the first line says what's wrong, and each indented line goes one step closer to the cause.
              • Fix the code instead of hiding the error. If you must hide one, use @ts-expect-error and say why.
              • To convert a file: rename it, type the parameters, describe the shapes, and fix what the errors find.

              Remember the one-liner.
              Write down what each value must be, read every error from where to what, and fix the code instead of hiding the message.

              Coming next: Primitive types. It covers string, number, boolean, and the other simple types you just started writing.

              CompleteFrontend Clear concepts. Working examples.