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.
- 01Annotate valuesWrite a type after a variable or a parameter, like
let total: number = 0, and say what it promises. - 02Read an error messageFind the file, line, and column, read the message in plain words, and follow a message that runs over several lines.
- 03Fix instead of hidingFix the code behind an error, know what
@ts-ignoreand@ts-expect-errorhide, and turn a.jsfile into a.tsfile 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:
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:
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.
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.
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-ignorehides the error, and the bug stays
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.
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:
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.
let itemCount: number = 2;itemCount = itemCount + 1;console.log(itemCount);: numberNo errorsprints 3itemCount holds 2, then 3. Both are numbers, so there are no errors, and line 3 prints 3.
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:
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:
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:
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:6Every error has the same parts. Read them in this order:
- Where:
src/cart.ts:6:23is the file, line 6, and column 23. A column counts characters from the start of the line, so 23 is wherepriceTextstarts. - Its number:
TS2345. Every kind of error has one. Search for it when a message is new to you. - What's wrong: the message itself, explained below.
- The spot: line 6 again, with
~~~~~~~~~under the exact part the message is about. Your editor draws the same thing as a red squiggle. - The count:
Found 1 error, and where it is.
~~~~~~~~~ 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.
function lineTotal(price: number, quantity: number) { return price * quantity;} const total = lineTotal("4", 2);console.log(total);TS2345src/cart.ts:5:25 - error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
5 const total = lineTotal("4", 2);
~~~src/cart.tsline 5, column 258Where: 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.
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:
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:
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.
| The message | In plain words | The 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:
function addToTotal(total: number, price: number) { return total + price;} const cakePrice = "4"; // typed into a form, so it's textconsole.log(addToTotal(3, cakePrice));341 error: line 6still thereThe 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.
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.
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.
| What you do | Is 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 line | No. | Nothing for that line, ever, even after a new mistake. |
Put // @ts-expect-error above the line | No. | Unused '@ts-expect-error' directive, once the line has no error. |
Turn strict off in tsconfig.json | No. | 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:
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)}`);Total: $NaN3 errors: lines 1, 1 and 5The 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.
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.
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-checkcomment at the top of a.jsfile 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:
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:
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:
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:
- Wrap the text in
Number(…) - Add
// @ts-ignoreabove the line - Write
price: numberon a parameter with no type - Add
// @ts-expect-errorabove the line - Change
qtytoquantityin the cake - Set
"strict": falsein tsconfig.json - Check
if (cake)before readingcake.price - Write
const cakePrice: any = "4";
Sort each change by what it does about the error.
Common mix-ups
- "An annotation converts the value." No.
let total: number = "10"is an error, not the number 10.Number("10")converts. - "
@ts-ignorefixes the error." No. It hides the message. The fix-or-hide playground still printed34. - "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
.jsto.tschanges 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.
tsc printed this line. Which line and column of the file is the error on?
src/checkout.ts:7:15 - error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.line 7, column 15src/checkout.ts:7:15 is the file, line 7, and column 15. TS2345 is the error's number, not a line.
Which line will TypeScript flag?
let itemCount: number = 0;
itemCount = itemCount + 2;
itemCount = "3";
console.log(itemCount);let itemCount: number = 0;itemCount = itemCount + 2;itemCount = 3; // line 3 had the error: "3" is textconsole.log(itemCount);Line 3 stores the text "3" in itemCount, which line 1 annotated as a number: Type 'string' is not assignable to type 'number'. Without the quotes, it's fixed.
Shipping costs 4 plus the parcel's weight. Fix line 6 without hiding the error. What does the fixed code print?
function shippingCost(weight: number) { return weight + 4;} const weightText = "5"; // from a formconsole.log(shippingCost(weightText));function shippingCost(weight: number) { return weight + 4;} const weightText = "5"; // from a formconsole.log(shippingCost(Number(weightText)));weightText is text, and weight takes a number. Number(weightText) passes the number 5, so the error is gone and 5 + 4 prints 9. The starter printed 54, because + joined the text.
TypeScript marks line 4 with a message over three lines. Which property is wrong? Press Edit & run to read the whole message.
type Profile = { name: string; age: number; newsletter: boolean };
const asha = { name: "Asha", age: 30, newsletter: "yes" };
const profile: Profile = asha;type Profile = { name: string; age: number; newsletter: boolean }; const asha = { name: "Asha", age: 30, newsletter: true };const profile: Profile = asha;The message says Types of property 'newsletter' are incompatible, then Type 'string' is not assignable to type 'boolean'. "yes" is text; a Profile needs true or false.
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.
function addTip(bill: number, tip: number) { return bill + tip;} const tipText = "2"; // from a form// @ts-ignoreconsole.log(addTip(10, tipText));function addTip(bill: number, tip: number) { return bill + tip;} const tipText = "2"; // from a formconsole.log(addTip(10, Number(tipText)));The comment hid Argument of type 'string' is not assignable to parameter of type 'number', and 10 + "2" printed 102. With the comment gone and Number(tipText), the bill is 12.
This was discount.js; now it's discount.ts, with strict on. Fix all three errors without hiding any. What does it print?
function discounted(total, percent) { return total - (total * percent) / 100;} const order = { total: 50, coupon: 10 };console.log(discounted(order.total, order.coupn));function discounted(total: number, percent: number) { return total - (total * percent) / 100;} const order = { total: 50, coupon: 10 };console.log(discounted(order.total, order.coupon));total: number, percent: number answer the two implicitly has an 'any' type errors. Line 6's message asks Did you mean 'coupon'?: as JavaScript, order.coupn was undefined, and the discount printed NaN. Fixed, 50 minus 10% prints 45.
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.
Question 1 of 7In
let total: number = 0;, what does: numberdo?Choose an answer to see the explanation.
Question 2 of 7Which line of src/cart.ts does this error point at?
Read the code, then predictTextsrc/cart.ts:12:9 - error TS2322: Type 'string' is not assignable to type 'number'.Choose an answer to see the explanation.
Question 3 of 7What does TypeScript say about line 5 of this code?
Read the code, then predictfunction lineTotal(price: number, quantity: number) { return price * quantity; } console.log(lineTotal("4", 2));Choose an answer to see the explanation.
Question 4 of 7This code hides its error with @ts-ignore. What does it print?
Read the code, then predictfunction 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.
Question 5 of 7Which comment speaks up once the line below it has no error left?
Choose an answer to see the explanation.
Question 6 of 7Which property does this error chain point to?
Read the code, then predicttype 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.
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-errorand 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.