cf.completefrontend
Open lab My progress
THE TYPESCRIPT FIELD GUIDE

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.

By the end, you can
  • 01
    Write an annotationPut a type after a variable, a parameter, or a function's brackets, and read the errors when a value doesn't fit.
  • 02
    Predict an inferred typeSay what type TypeScript works out from a starting value, a calculation, a return, or the place a callback is passed.
  • 03
    Choose 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:

One type you write, one TypeScript works outPop out in the code editor (opens in a new tab)TypeScript
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.

Left: the box itemCount holds 3, and you wrote its type, number, after a colon. Right: the box cartTotal holds 42, and an arrow from the 42 card shows TypeScript reading it and giving the type number. Both end with the type number.let itemCount: number = 33itemCounttype: numberyou wrote itlet cartTotal = 4242cartTotaltype: numberTypeScript read it from 42
On the left you wrote : 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:

Both types are checked (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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.

Real-life analogyPrice labels at a market stall

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 = 42 gets number
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.parse result needs an annotation
The main idea

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:

Annotations on variables and a functionPop out in the code editor (opens in a new tab)TypeScript
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: userName is a string, and "Asha" fits.
  • Line 2 annotates a variable with no starting value. age has a type now, and line 3 gives it a value that fits it.
  • Line 5 has two annotations. name: string inside the brackets is a parameter annotation: the function must be given text. : string after the brackets is a return type annotation: the function promises to give back text (Functions covers parameters and return).
  • 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:

Reading a value before storing it (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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.

Playground: write the annotation
A quantity with an annotationPop out in the code editor (opens in a new tab)TypeScript
let quantity: number = 3;quantity = quantity + 1;console.log(quantity);
Line 1 saysquantity: number
What happens
TypeScriptNo errors
Try it yourself

3 is a number and so is quantity + 1, so both lines match the annotation. No errors, and the code prints 4.

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 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:

Types worked out from values and sumsPop out in the code editor (opens in a new tab)TypeScript
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:

Playground: change the starting value
A discount with no type writtenPop out in the code editor (opens in a new tab)TypeScript
let discount = 5;//  ^?discount = 10;console.log(discount);
Inferred typenumber
What happens
Starting value5
TypeScriptNo errors
Try it yourself

The starting value is the number 5, so TypeScript infers number. Line 3 stores 10, another number: no errors, and it prints 10.

TypeScript checks the code for real, and the query on line 2 shows the type it inferred. Each button changes only the starting value on line 1. Reset goes back to 5.

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):

A click listener's event, with no type writtenPop out in the code editor (opens in a new tab)TypeScript
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.

An editor shows buyButton.addEventListener("click", (event) => ...). An arrow goes from the word click to a card that says event: PointerEvent.editorbuyButton.addEventListener("click", (event) => …)event: PointerEventworked out from "click"
You never wrote a type for 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:

The types of a function and its resultPop out in the code editor (opens in a new tab)TypeScript
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.

An editor window with the line const teaTotal = lineTotal(3, 2). A box below the name teaTotal says const teaTotal: number. A note says: point at a name to see its type.editorconst teaTotal = lineTotal(3, 2);const teaTotal: numberpoint at a nameto see its type
Point at 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:

A parameter with no type (an error on purpose)Pop out in the code editor (opens in a new tab)TypeScript
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:

The parameter, annotatedPop out in the code editor (opens in a new tab)TypeScript
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:

Playground: where the error shows up
A shipping cost with a mistake on line 3Pop out in the code editor (opens in a new tab)TypeScript
function shippingCost(total: number) {  if (total >= 50) return 0;  return "4.99";}const cost = shippingCost(30);//    ^?const toPay: number = 30 + cost;console.log(toPay);
TypeScript marks1 error: line 7
What happens
Type of cost0 | "4.99"
When it runsprints 304.99
Try it yourself

With 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'.

TypeScript checks the code for real each time. Only line 1 changes. When it runs, both versions print 304.99, because line 3 returns text. Reset goes back to No return type.

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.

Left, no return type: a window shows return "4.99" on line 3 and 30 + cost on line 7, with the error mark on line 7. Right, with : number: the same lines, with the error mark on line 3.no return typeline 3: return "4.99";line 7: 30 + costerror on line 7, far awaywith : numberline 3: return "4.99";line 7: 30 + costerror on line 3, at the mistake
The mistake is always line 3, which returns text. Without a return type, TypeScript marks line 7, where the cost is used. With : 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:

Saved settings read with JSON.parsePop out in the code editor (opens in a new tab)TypeScript
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.

Where an annotation is worth writing
When you writeAnnotate 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 = 0No.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 seeYes.TypeScript only knows it as any, which turns checking off.
A function's return typeSometimes.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:

Reading a quantity box with one typed functionPop out in the code editor (opens in a new tab)TypeScript
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.

Error or fine?
  • 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);
Try it yourself
0 of 8 correct

Sort each line by what TypeScript says about it. Each line is checked on its own, in strict mode.

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

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.parse result 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.

Exercise 1 · Warm-upRead an inferred type

No type is written here. What type does TypeScript give total? Guess, then check.

Starter codePop out in the code editor (opens in a new tab)TypeScript
const total = 3 * 4;console.log(total);

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

    Exercise 2 · Warm-upFind the line TypeScript marks

    There are no annotations here. Which line will TypeScript mark anyway?

    Starter codePop out in the code editor (opens in a new tab)TypeScript
    let cartTotal = 42;
    cartTotal = 50;
    cartTotal = "fifty";
    console.log(cartTotal);

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

      Exercise 3 · PracticeAnnotate a parameter

      TypeScript marks line 1. Add one annotation to fix it. What does the fixed code print?

      Starter codePop out in the code editor (opens in a new tab)TypeScript
      function addTax(price) {  return price * 1.2;}console.log(addTax(10));

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

        Exercise 4 · PracticeMove the error to the mistake

        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?

        Starter codePop out in the code editor (opens in a new tab)TypeScript
        function shippingCost(total: number) {  if (total >= 50) return 0;  return "4.99";}console.log(shippingCost(30) + 1);

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

          Exercise 5 · ChallengeName a listener's event type

          A search box listens for key presses. No type is written for event. What type does TypeScript give it?

          Starter codePop out in the code editor (opens in a new tab)TypeScript
          const searchBox = document.createElement("input");searchBox.addEventListener("keydown", (event) => {  console.log(event.key);});

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

            Exercise 6 · ChallengeRemove the annotations you don't need

            This code has six annotations. Remove every one TypeScript doesn't need, with no errors left. How many must stay?

            Starter codePop out in the code editor (opens in a new tab)TypeScript
            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));

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

              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.

              The annotations and inference quiz · 7 questionsScore: first tries count
              1. Question 1 of 7What type does TypeScript give count on line 1?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                let count = 0;
                count = count + 1;

                Choose an answer to see the explanation.

              2. Question 2 of 7No type is written here. Which line does TypeScript flag?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                let price = 4;
                price = 5;
                price = "6";

                Choose an answer to see the explanation.

              3. Question 3 of 7How strictly does TypeScript check a type it inferred?

                Choose an answer to see the explanation.

              4. Question 4 of 7In strict mode, which of these needs an annotation?

                Choose an answer to see the explanation.

              5. Question 5 of 7What type does event get in this keydown listener?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                document.addEventListener("keydown", (event) => {
                  console.log(event.key);
                });

                Choose an answer to see the explanation.

              6. Question 6 of 7What does hovering half show?

                Read the code, then predictPop out in the code editor (opens in a new tab)TypeScript
                function half(n: number) {
                  return n / 2;
                }

                Choose an answer to see the explanation.

              7. 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.parse results, 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.

              CompleteFrontend Clear concepts. Working examples.