Type annotations & inference
Learn where to write a TypeScript type annotation, how TypeScript infers the rest from your values, how to see any type, and when an annotation is worth it.
- 01Write an annotationPut a type after a variable, a parameter, or a function's brackets, and read the errors when a value doesn't fit.
- 02Predict an inferred typeSay what type TypeScript works out from a starting value, a calculation, a return, or the place a callback is passed.
- 03Choose when to annotateAnnotate parameters, values TypeScript can't see, and promises like return types, and leave the rest to inference.
Two ways a value gets its type
Every name in TypeScript has a type. A shopping cart needs a count of items and a total, and both must be numbers. There are two ways a name gets its type: you write it, or TypeScript works it out from the value. Here are both, side by side:
let itemCount: number = 3;// ^?let cartTotal = 42;// ^?itemCount = 4;cartTotal = 58;console.log(itemCount, cartTotal);Line 1 says itemCount is a number with : number after the name. Writing a type like that is called a type annotation. Line 3 makes cartTotal with no type written at all.
Lines 2 and 4 are queries. A comment like // ^? asks "what type is the name my ^ points at?", and the page writes TypeScript's answer after it. Both answers are the same: let itemCount: number and let cartTotal: number. For cartTotal, TypeScript read the starting value, 42, and worked out number by itself. That is called type inference: "infer" means to work something out from what you can see. Lines 5 and 6 store new numbers, and line 7 prints 4 58.
: number. On the right TypeScript read the starting value, 42, and worked out number itself. Both boxes end up with the same type, checked the same way.An inferred type isn't a weaker kind of type. TypeScript checks both names in exactly the same way:
let itemCount: number = 3;let cartTotal = 42; itemCount = "four";cartTotal = "free";Line 4 tries to put the text "four" in itemCount, and line 5 tries to put "free" in cartTotal. TypeScript marks both lines, on purpose here, with the same message: Type 'string' is not assignable to type 'number'. It means "you're putting text where a number has to go". Line 5 gets the error even though nobody wrote number for cartTotal.
At a market stall, some bags get a label the owner writes by hand. Most go on the scale, which weighs them and prints the label itself. At the till, the cashier reads both kinds of label the same way. Only a closed parcel, which the scale can't look inside, has to be labelled by hand.
- In real life: The stall owner writes a label by hand: 3 kg
- In TypeScript: You write an annotation:
let itemCount: number = 3 - In real life: The scale weighs the apples and prints the label
- In TypeScript: TypeScript reads the value:
let cartTotal = 42getsnumber - In real life: The cashier checks both labels the same way
- In TypeScript: TypeScript checks annotated and inferred types the same way
- In real life: A closed parcel the scale can't see inside needs a written label
- In TypeScript: A parameter or a
JSON.parseresult needs an annotation
An annotation is a type you write; inference is a type TypeScript works out. You write an annotation after a name, with a colon: itemCount: number. When you don't, TypeScript infers the type from the value. Either way, the type is checked the same way.
Writing an annotation
An annotation always goes after the thing it describes, behind a colon. Some languages put the type first, like int count; TypeScript never does. Here are the places you'll write one, in a small user profile:
let userName: string = "Asha";let age: number;age = 31; function greet(name: string): string { return `Hi, ${name}`;}console.log(greet(userName), age);Line by line:
- Line 1 annotates a variable with a starting value:
userNameis astring, and"Asha"fits. - Line 2 annotates a variable with no starting value.
agehas a type now, and line 3 gives it a value that fits it. - Line 5 has two annotations.
name: stringinside the brackets is a parameter annotation: the function must be given text.: stringafter the brackets is a return type annotation: the function promises to give back text (Functions covers parameters andreturn). - Line 6 returns a template literal, which is a string, so the promise holds. Line 8 prints
Hi, Asha 31.
An annotation with no starting value gives a name a type, not a value. TypeScript keeps track of whether you stored one before you read it:
let age: number;console.log(age);age = 31;Line 1 says age is a number. Line 2 reads it, but nothing has been stored yet. TypeScript marks line 2 on purpose: Variable 'age' is used before being assigned. "Assigned" means "given a value". When it runs, line 2 prints undefined, which isn't a number at all. Line 3 comes too late.
The annotation decides the type, and the value has to fit it. Try it: type a different type after the colon on line 1, and watch the list under the code as you type.
let quantity: number = 3;quantity = quantity + 1;console.log(quantity);quantity: numberNo errors3 is a number and so is quantity + 1, so both lines match the annotation. No errors, and the code prints 4.
With string, the number 3 no longer fits. With boolean, line 2's sum breaks too, because TypeScript now believes quantity is true or false. A misspelled type, like numbr, gets Cannot find name 'numbr', and the message suggests number. The capital Number is the wrapper object type from the Primitive types lesson, which can't be used in a sum. An empty box gets Type expected, because a colon must be followed by a type.
How inference works
Most of the time you don't write a type, and TypeScript infers it. It doesn't guess. It follows fixed rules, and the first one is simple: a name gets the type of the value it starts with. The type of a calculation follows from what goes into it:
const price = 4;const quantity = 3;const total = price * quantity;// ^?const label = "Total: " + total;// ^?const isFree = total === 0;// ^?console.log(label, isFree);Lines 1 and 2 store a price and a quantity. Line 3 multiplies them, and the query on line 4 shows const total: number: a number times a number is a number. Line 5 joins text and a number, which makes text, so the query on line 6 shows string. Line 7 compares two numbers with ===, which gives true or false, so the query on line 8 shows boolean. Line 9 prints Total: 12 false. You wrote no types, and every name has one.
The starting value decides the type, and later values must fit it. Press each button to give discount a different starting value, and watch the query on line 2:
let discount = 5;// ^?discount = 10;console.log(discount);number5No errorsThe starting value is the number 5, so TypeScript infers number. Line 3 stores 10, another number: no errors, and it prints 10.
5 and 2.5 both give number, so line 3's 10 fits. "5" gives string, true gives boolean, and 5n gives bigint, and each time line 3 gets an error. Nobody wrote a type, but the one TypeScript inferred is enforced. (A const keeps its exact value as its type, like const minOrder: 50, as the Primitive types lesson showed. This playground uses let, so the type is the wider number.)
Inference also works the other way round. Sometimes TypeScript knows a value's type from where you put it. This is called contextual typing: the context, meaning the place the code sits, gives the type. The most common case is a callback, a function you pass to another function, like the listener you pass to addEventListener (Events covers listeners):
const buyButton = document.createElement("button");buyButton.addEventListener("click", (event) => {// ^? console.log(event.clientX);});Line 1 makes a Buy button. Line 2 adds a click listener, an arrow function that takes event. Nobody wrote a type for event, but the query on line 3 shows (parameter) event: PointerEvent. TypeScript's description of the browser says a "click" listener is called with a PointerEvent, the object that describes a click or tap. That's why line 4 may read event.clientX, the distance of the click from the left edge of the visible page.
event. addEventListener knows that a "click" comes with a PointerEvent, so the callback's event gets that type.Hover to see a type
Inferred types aren't hidden. In your editor, point at any name and a small box shows its type. The ^? lines in these lessons show the same answer on the page:
function lineTotal(price: number, quantity: number) {// ^? return price * quantity;}const teaTotal = lineTotal(3, 2);// ^?const label = `Tea: $${teaTotal}`;// ^?console.log(label);The query on line 2 points at lineTotal and shows the whole function: function lineTotal(price: number, quantity: number): number. You wrote the two parameter types. The : number at the end is the return type, and TypeScript inferred it from line 3, because a number times a number is a number. The query on line 6 shows const teaTotal: number, and the one on line 8 shows const label: string. Line 9 prints Tea: $6.
teaTotal in your editor and a box shows const teaTotal: number. The ^? lines in these lessons show the same answer on the page.Three ways to see a type, depending on where you are:
- In your editor: point at a name with the mouse. In VS Code you can also put the cursor on it and press Ctrl+K, then Ctrl+I (Cmd+K, Cmd+I on a Mac).
- In these lessons: point at a name in a checked example, or read the answer after a
^?line. - In Edit & run: move the cursor onto a name, and its type shows under the editor. You can also add your own
// ^?line, with the^under the name you want to ask about.
Press Edit & run on the example above and try it: add a line // ^? under line 3 with the ^ under price, and the answer is (parameter) price: number. Checking a type this way is the quickest way to learn inference: guess first, then hover.
When annotations help
Since TypeScript infers so much, when should you write a type? Mostly where TypeScript has nothing to read the type from. The first such place is a function's parameters:
function addTax(price) { return price * 1.2;}console.log(addTax(10));Line 1 declares addTax with a price parameter and no type. TypeScript marks it on purpose: Parameter 'price' implicitly has an 'any' type. In plain words: "I can't tell what price is, so I'd have to give up checking it". any is the type that turns checking off; the any, unknown & never lesson covers it. TypeScript doesn't look at the call on line 4 to work out the parameter's type: the function could be called from anywhere. The code still runs and prints 12.
This error comes from the noImplicitAny option, part of strict, which every example in these lessons uses. The fix is one annotation:
function addTax(price: number) { return price * 1.2;}console.log(addTax(10));Line 1 now says price: number. The error is gone, line 2 is checked as number math, and line 4 still prints 12. You didn't write a return type: TypeScript infers number from line 2.
A return type is never required, because TypeScript infers it from the return lines. But writing one moves errors to a better place. This shipping cost has a mistake: line 3 returns the text "4.99" instead of a number. Switch line 1 between no return type and : number, and watch where the error lands:
function shippingCost(total: number) { if (total >= 50) return 0; return "4.99";}const cost = shippingCost(30);// ^?const toPay: number = 30 + cost;console.log(toPay);1 error: line 70 | "4.99"prints 304.99With no return type, TypeScript infers one from the two return lines: 0 | "4.99", read "either 0 or the text 4.99". The mistake is on line 3, but the error lands on line 7, where the cost is used: Operator '+' cannot be applied to types 'number' and 'string | number'.
With no return type, TypeScript believes the function gives back what it returns, text included, and the error appears on line 7, far from the mistake. With : number, the function makes a promise, and TypeScript checks every return against it, so line 3 itself gets the error. In a big project, the line that uses a value can be in a different file from the line that made it.
: number, it marks line 3 itself.The last common place is a value from outside your code. JSON.parse reads text, so TypeScript can't know what it gives back (JSON covers it). Here are saved settings, read twice:
type Settings = { theme: string; fontSize: number }; const saved = '{ "theme": "dark", "fontSize": 18 }';const parsed = JSON.parse(saved);// ^?const settings: Settings = JSON.parse(saved);// ^?console.log(settings.fontSize + 2);Line 1 names the shape of the settings with a type alias: type Settings = … gives a name to an object type (Object types covers them). Line 3 is the saved text. Line 4 parses it with no annotation, and the query on line 5 shows const parsed: any: TypeScript has nothing to infer from, so it gives up. Line 6 parses it again with : Settings, and the query on line 7 shows const settings: Settings. From here on, settings.fontSize is a number and settings.color would be an error. Line 8 prints 20.
The annotation on line 6 is a promise you make, not a check: if the saved text had a different shape, TypeScript couldn't notice, because the text only exists while the page runs. The table below sums up where to write a type. Read each row as: this kind of name, should you annotate it, and why.
| When you write | Annotate it? | Why |
|---|---|---|
A function parameter, like price in addTax(price) | Yes. | Nothing tells TypeScript what will be passed in, so strict mode reports an error without one. |
A variable with a starting value, like let total = 0 | No. | TypeScript reads the type from the value, and checks it as strictly as a written type. |
A variable you fill in later, like let age; | Yes. | With no starting value, TypeScript can't settle on one type, so let age; takes a number and then text without an error. |
A value from JSON.parse or another source TypeScript can't see | Yes. | TypeScript only knows it as any, which turns checking off. |
| A function's return type | Sometimes. | TypeScript infers it, but writing it makes TypeScript check each return against your promise. |
A callback's parameter, like event in addEventListener("click", (event) => …) | No. | The place the callback is passed already says what type it gets. |
A quantity box, typed at the edges
Put it together on a checkout page. The shopper types a quantity into a box, and the page reads it. A good habit is to annotate the edges of your code, where values come in and go out, like a function's parameters and return type, and let inference fill in the middle:
const quantityInput = document.createElement("input");// ^?quantityInput.value = "2"; function readQuantity(input: HTMLInputElement): number { return Number(input.value);} const quantity = readQuantity(quantityInput);// ^?console.log(quantity + 1);Line 1 makes an <input> box, and the query on line 2 shows TypeScript inferred HTMLInputElement, the type of an input element, from the "input" you passed in. Line 3 puts "2" in it, as if the shopper typed 2 (Form elements covers .value). Line 5 is the edge: readQuantity takes an HTMLInputElement and promises a number. Line 6 keeps the promise by turning the text into a number. Line 9 has no annotation, and the query on line 10 shows const quantity: number, worked out from the return type. Line 11 prints 3.
There are only two annotations in the whole example, both on line 5. Every other type was inferred, and every one is checked.
let total = 0; total = "free";let total: number; total = 5;function double(n) { return n * 2; }function double(n: number) { return n * 2; }const label: string = 42;let label = "Tea"; label = "Coffee";let age: number; console.log(age);document.addEventListener("click", (event) => event.clientX);
Sort each line by what TypeScript says about it. Each line is checked on its own, in strict mode.
Common mix-ups
- "An inferred type is only a guess, so it isn't checked." No. Inference follows fixed rules, and the result is checked exactly like a type you wrote.
let total = 0; total = "free";is an error. - "Annotating every variable makes code safer." No. On a variable with a starting value, the annotation usually says what TypeScript already knows. It adds words to read and change, not checks.
- "TypeScript works out a parameter's type from how I call the function." No. It never looks at the calls. A parameter needs an annotation, unless the function is passed somewhere that gives it one, like a listener.
- "An annotation turns the value into that type." No.
const price: number = "4"doesn't make a number; it gets an error. Annotations are removed before the code runs, so they can't change a value. - "Annotating a
JSON.parseresult checks the data." No. TypeScript trusts your annotation. It can't see data that only arrives while the page runs. - "The type goes first, like
string city." No. In TypeScript the type always comes after the name, behind a colon:city: string.
Practice exercises
Each exercise is TypeScript. Press Edit & run on the starter code to try your answer: the errors update as you type, the type under the cursor shows below the editor, and the console shows what it prints. Guess first, then use the hints.
No type is written here. What type does TypeScript give total? Guess, then check.
const total = 3 * 4;console.log(total);const total = 3 * 4;// ^?console.log(total);3 * 4 multiplies two numbers, and that gives a number, so TypeScript infers number. The query on line 2 shows const total: number.
There are no annotations here. Which line will TypeScript mark anyway?
let cartTotal = 42;
cartTotal = 50;
cartTotal = "fifty";
console.log(cartTotal);let cartTotal = 42;cartTotal = 50;cartTotal = 55;console.log(cartTotal);Line 1 starts cartTotal at 42, so its inferred type is number. Line 3 stores the text "fifty", and TypeScript says Type 'string' is not assignable to type 'number'. Storing a number, like 55, fixes it.
TypeScript marks line 1. Add one annotation to fix it. What does the fixed code print?
function addTax(price) { return price * 1.2;}console.log(addTax(10));function addTax(price: number) { return price * 1.2;}console.log(addTax(10));price: number tells TypeScript what the function takes, so the implicitly has an 'any' type error is gone. TypeScript infers the return type, number. 10 * 1.2 is 12.
The error is on line 5, but the mistake is somewhere else. Add a return type so TypeScript points at the mistake, then fix it. What does the fixed code print?
function shippingCost(total: number) { if (total >= 50) return 0; return "4.99";}console.log(shippingCost(30) + 1);function shippingCost(total: number): number { if (total >= 50) return 0; return 4.99;}console.log(shippingCost(30) + 1);With : number on line 1, TypeScript marks line 3, where the text is returned. Returning the number 4.99 keeps the promise, and 4.99 + 1 prints 5.99.
A search box listens for key presses. No type is written for event. What type does TypeScript give it?
const searchBox = document.createElement("input");searchBox.addEventListener("keydown", (event) => { console.log(event.key);});const searchBox = document.createElement("input");searchBox.addEventListener("keydown", (event) => {// ^? console.log(event.key);});TypeScript's description of the browser says a "keydown" listener gets a KeyboardEvent, so event gets that type from where the function is passed. That's why event.key is allowed. The query on line 3 shows (parameter) event: KeyboardEvent.
This code has six annotations. Remove every one TypeScript doesn't need, with no errors left. How many must stay?
const taxRate: number = 0.25;let itemCount: number = 3;function lineTotal(price: number, quantity: number): number { return price * quantity;}const total: number = lineTotal(4, itemCount);console.log(total * (1 + taxRate));const taxRate = 0.25;let itemCount = 3;function lineTotal(price: number, quantity: number) { return price * quantity;}const total = lineTotal(4, itemCount);console.log(total * (1 + taxRate));Only price: number and quantity: number must stay: without them, each parameter gets implicitly has an 'any' type. Every other type is inferred and still checked. (taxRate is a const, so it gets its exact value, 0.25, as its type, which works anywhere a number goes.) The code still prints 15.
Check your understanding
For each question, ask: did someone write the type, and if not, what could TypeScript read it from? The quiz shows its code without the checker, so the error list doesn't give the answers away.
Question 1 of 7What type does TypeScript give count on line 1?
Read the code, then predictlet count = 0; count = count + 1;Choose an answer to see the explanation.
Question 2 of 7No type is written here. Which line does TypeScript flag?
Read the code, then predictlet price = 4; price = 5; price = "6";Choose an answer to see the explanation.
Question 3 of 7How strictly does TypeScript check a type it inferred?
Choose an answer to see the explanation.
Question 4 of 7In strict mode, which of these needs an annotation?
Choose an answer to see the explanation.
Question 5 of 7What type does event get in this keydown listener?
Read the code, then predictdocument.addEventListener("keydown", (event) => { console.log(event.key); });Choose an answer to see the explanation.
Question 6 of 7What does hovering half show?
Read the code, then predictfunction half(n: number) { return n / 2; }Choose an answer to see the explanation.
Question 7 of 7Which line gives city the type string?
Choose an answer to see the explanation.
Key takeaways
- An annotation is a type you write after a name, behind a colon:
let itemCount: number,function greet(name: string): string. - Inference is TypeScript working the type out from the value. An inferred type is checked exactly like one you write.
- A variable gets the type of its starting value. A calculation gets the type of its result. A function passed somewhere, like a listener, gets its parameter types from that place.
- Point at a name in your editor, or use a
^?query, to see any type. - Annotate parameters, values TypeScript can't see into, like
JSON.parseresults, and return types when you want errors at the mistake. Let inference do the rest.
Remember the one-liner.
Write types where values come in; let TypeScript work out the rest, and check both the same way.
Coming next: Arrays & tuples. It shows how to type a list of values, and a list with a fixed shape.