Skip to content
aviral gupta

// B2.2 · ~30 min · Beginner

Annotations versus inference

After this lesson you can annotate variables, parameters and return types, predict what TypeScript infers without them, and leave out the annotations that only repeat it.

Lesson 2 of 5 in B2 Everyday types

You will be able to

  • Annotate variables, parameters and return types, and say where each annotation goes
  • Predict the type TypeScript infers from a value, a mixed array and a function's return statements
  • Recognise contextual typing in callbacks, and decide when an annotation is needed
  1. Warm-up · Activity 1 of 7

    Warm-up from the last lessons: which of these lines contain a type annotation? Pick all that apply.

    Select all that apply.

  2. Predict · Activity 2 of 7

    Predict before you read on: which type does TypeScript infer for mixed?

    const mixed = [1, "two", 3];
  3. Practice · Activity 3 of 7

    Fill in the return type, so that tsc accepts the function and it prints 3.5.

    function average(a: number, b: number): ____ {
      return (a + b) / 2;
    }
    
    console.log(average(3, 4));
    function average(a: number, b: number): {
  4. Practice · Activity 4 of 7

    The callback has no annotation. Which type does TypeScript infer for lengths?

    const names = ["Ada", "Grace"];
    const lengths = names.map((name) => name.length);
  5. Practice · Activity 5 of 7

    The return type says string. What does tsc report?

    function grade(points: number): string {
      if (points >= 50) {
        return "pass";
      }
    }
  6. Brain teaser · Activity 6 of 7

    Brain teaser. Both lines hold the same arrow function (n) => n * 2, and neither n has an annotation. Which lines does tsc report?

    const double = (n) => n * 2;
    console.log([1, 2, 3].map((n) => n * 2));
  7. Apply · Activity 7 of 7

    Mini-task. Start from const words = ["tree", "sky", "river"]. Keep the words longer than 3 letters with filter, turn them into capitals with map, and join them with a function joinAll(items: string[]): string. Annotate only what needs it: joinAll's parameter and return type. The callbacks and the variables get no annotation. Run npx tsc and node main.ts; it prints TREE, RIVER. Then misspell toUpperCase in the map callback and read the error: contextual typing knew word was a string.

    Check your work against this list

Build it yourself

Read the worked example, then write the exercises. Your code runs in your browser or on your computer and is never uploaded.

Worked example

An order total, half annotated

The comments say which types are written and which TypeScript infers. The parameters of the two declared functions are annotated; prices, label, the return type of sum and the callback's price are inferred. Run npx tsc, which prints nothing, then node main.ts. Then try label = 42 or prices.push("free"): tsc reports both, although neither variable has an annotation.

main.ts

// Which types are written, and which does TypeScript infer?
const prices = [4.5, 12, 3.25]; // inferred: number[]
const discount: number = 0.1; // annotated, though 0.1 would give number anyway
let label = "Order"; // inferred: string

function applyDiscount(amount: number, rate: number): number {
  return amount * (1 - rate);
}

// No return type written: TypeScript infers number from the return statement.
function sum(values: number[]) {
  let total = 0;
  for (const value of values) {
    total += value;
  }
  return total;
}

// price needs no annotation: map passes the elements of a number[].
const discounted = prices.map((price) => applyDiscount(price, discount));
label = label + " #42";
console.log(label);
console.log("before:", sum(prices));
console.log("after:", sum(discounted).toFixed(2));

Run it with

npx tsc
node main.ts

Output

Order #42
before: 19.75
after: 17.78
  • prices is number[] and label is string, both inferred from their first values.
  • sum has no return type, yet sum(discounted).toFixed(2) type-checks: TypeScript inferred number.
  • price in the map callback is a number from its context, so applyDiscount(price, discount) is checked.
  • The discount annotation changes nothing; it only repeats what 0.1 would give.
Change it and run it

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.

Exercises

Exercise 1 of 2

Annotate only what needs it

Both functions work, but tsc reports four times Parameter … implicitly has an 'any' type. Annotate the three parameters of the declared functions: values is a list of numbers, words a list of text, minLength a number. Do not annotate the callback's word: once words has its type, the fourth error disappears by itself. Return types are optional here.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    A list of numbers is number[], a list of text is string[].

  2. Hint 2

    The callback (word) => … gets its type from words.filter: once words is a string[], word is a string.

  3. Hint 3

    The first lines become average(values: number[]) and longWords(words: string[], minLength: number).

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export function average(values: number[]): number {
  let sum = 0;
  for (const value of values) {
    sum += value;
  }
  return sum / values.length;
}

export function longWords(words: string[], minLength: number): string[] {
  return words.filter((word) => word.length >= minLength);
}
Run it on your computer

Install TypeScript 7.0 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.ts

export function average(values) {
  let sum = 0;
  for (const value of values) {
    sum += value;
  }
  return sum / values.length;
}

export function longWords(words, minLength) {
  return words.filter((word) => word.length >= minLength);
}

main.test.ts

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {average, longWords} from './main.ts';

test('average([2, 4, 9]) is 5', () => {
  assert.equal(average([2, 4, 9]), 5, 'average([2, 4, 9]) should be 5');
});

test('longWords keeps the words with at least 3 letters', () => {
  assert.deepEqual(longWords(['a', 'tree', 'sky'], 3), ['tree', 'sky'], 'longWords(["a", "tree", "sky"], 3) should be ["tree", "sky"]');
});

package.json

{
  "type": "module"
}

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "lib": [
      "esnext",
      "dom"
    ],
    "types": [],
    "strict": true,
    "noEmit": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "allowImportingTsExtensions": true
  },
  "include": [
    "**/*.ts"
  ],
  "exclude": [
    "**/*.test.ts"
  ]
}

npx tsc needs TypeScript in the folder: run npm install --save-dev typescript there once. tsc checks the types; Node.js runs main.ts by removing them.

Run the program:

npx tsc
node main.ts

Run the checks (needs learnrun.js in the same folder):

npx tsc
node --test
Download learnrun.js

Exercise 2 of 2

Let the return type find the bug

formatScore should always return text: "50% (pass)" from 50 percent up, and for example "25% (fail)" below. Its return type is annotated as string, and tsc reports the line that breaks that promise. Fix that line so that it returns the percentage followed by % (fail). Leave the annotations as they are.

Tab indents and Shift+Tab outdents. To leave the editor with the keyboard, press Esc, then Tab.

The first run downloads the TypeScript compiler (up to 2.1 MB) and keeps it cached. Your code stays on your device.

Hints
  1. Hint 1

    tsc reports main.ts(6,3): error TS2322: Type 'number' is not assignable to type 'string'. Line 6 returns the bare number.

  2. Hint 2

    Build the text the same way as on line 4, with toFixed(0) and a suffix.

  3. Hint 3

    Line 6 becomes return percent.toFixed(0) + "% (fail)";

Show a solution

One way to solve it. Yours can look different and still pass the checks.

export function formatScore(points: number, max: number): string {
  const percent = (points / max) * 100;
  if (percent >= 50) {
    return percent.toFixed(0) + "% (pass)";
  }
  return percent.toFixed(0) + "% (fail)";
}
Run it on your computer

Install TypeScript 7.0 or newer. Save these files in one folder, open a terminal in that folder, and run the commands below.

main.ts

export function formatScore(points: number, max: number): string {
  const percent = (points / max) * 100;
  if (percent >= 50) {
    return percent.toFixed(0) + "% (pass)";
  }
  return percent;
}

main.test.ts

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {formatScore} from './main.ts';

test('20 of 40 is a pass', () => {
  assert.equal(formatScore(20, 40), '50% (pass)', 'formatScore(20, 40) should be "50% (pass)"');
});

test('10 of 40 is a fail', () => {
  assert.equal(formatScore(10, 40), '25% (fail)', `formatScore(10, 40) should be "25% (fail)", got ${JSON.stringify(formatScore(10, 40))}`);
});

package.json

{
  "type": "module"
}

tsconfig.json

{
  "compilerOptions": {
    "target": "esnext",
    "module": "nodenext",
    "lib": [
      "esnext",
      "dom"
    ],
    "types": [],
    "strict": true,
    "noEmit": true,
    "erasableSyntaxOnly": true,
    "verbatimModuleSyntax": true,
    "allowImportingTsExtensions": true
  },
  "include": [
    "**/*.ts"
  ],
  "exclude": [
    "**/*.test.ts"
  ]
}

npx tsc needs TypeScript in the folder: run npm install --save-dev typescript there once. tsc checks the types; Node.js runs main.ts by removing them.

Run the program:

npx tsc
node main.ts

Run the checks (needs learnrun.js in the same folder):

npx tsc
node --test
Download learnrun.js

Common mistakes

An arrow function with no context

const double = (n) => n * 2;
console.log(double(21));

What tsc or Node.js prints

main.ts(1,17): error TS7006: Parameter 'n' implicitly has an 'any' type.

Why, and the fix

Contextual typing only works where the function is passed somewhere that says what its parameters are, such as map or forEach. A function stored in a variable has no such context, so TypeScript cannot infer n, and strict reports it. Annotate the parameter: const double = (n: number) => n * 2. The return type is then inferred as number.

Doing arithmetic with an element of a mixed array

const scores = [90, "85", 70];
const doubled = scores[1] * 2;
console.log(doubled);

What tsc or Node.js prints

main.ts(2,17): error TS2362: The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.

Why, and the fix

The best common type of 90, "85" and 70 is (string | number)[], so every element might be text, and * needs a number on the left. JavaScript would quietly print 170. Fix the data, not the check: write 85 without quotes, and scores becomes number[]. If the text really comes from outside, convert it once with Number("85").

A body that breaks its return type

function isAdult(age: number): boolean {
  return age >= 18 ? "yes" : "no";
}

console.log(isAdult(20));

What tsc or Node.js prints

main.ts(2,22): error TS2322: Type 'string' is not assignable to type 'boolean'.

Why, and the fix

The annotation promises a boolean, and tsc checks each value returned against it: "yes" (column 22) and "no" (column 30) are both reported. Without the annotation, TypeScript would infer a string return type and callers would get "yes" instead of true. Return the comparison itself: return age >= 18;

TypeScript in the browser: the TypeScript 6.0.3 compiler, Apache-2.0, then your browser’s own engine. Licence and source

Exit ticket

5 questions, no hints. Score 80% or more to complete the lesson.

Finish every activity above to unlock the exit ticket.

Report a problem

Spotted something wrong or unclear? Say what, and it will be checked and fixed.

#

At least 20 characters.

Only if you want a reply.

Key ideas

Where annotations go, and which ones you need

An annotation always comes after the thing it types: let total: number = 0, a parameter as (price: number), and a return type after the parameter list: function isAdult(age: number): boolean. Parameters of a function you declare need one; without it, strict reports error TS7006. A return type is optional, because TypeScript infers it from the return statements, but writing it makes tsc check the body against it: return "yes" in a boolean function is reported at the return, and a path that returns nothing gives error TS2366. A variable with a value rarely needs one: TypeScript infers the type from the value, and an annotation that says the same only repeats it.

What TypeScript infers

An unannotated variable gets the type of its first value: let count = 3 is a number, and assigning text later is an error. A function without a return type gets the type of what it returns: function half(n: number) { return n / 2; } returns number. For an array, TypeScript checks every element for a best common type: [1, 2, 3] is number[], but [1, "two", 3] is (string | number)[], and [0, 1, null] is (number | null)[]. So with scores = [90, "85", 70], scores[1] * 2 is an error. A const gets an even narrower type: const limit = 3 has the type 3, a literal type (lesson B2.5).

Contextual typing: callbacks need no annotations

Inference also works from the outside in. In names.map((name) => name.length), where names is a string[], TypeScript knows that map passes each element to the callback, so name is a string and the result is number[]. That is contextual typing: where a function stands decides its parameter types. It checks the callback's body too: name.toUppercase() is reported with Did you mean 'toUpperCase'?. The same arrow function stored on its own, const double = (n) => n * 2, has no context, so n is error TS7006. A rule of thumb: annotate the parameters of functions you declare, and return types where you want the body checked; leave callbacks and initialised variables to inference.

Sources

Last reviewed September 30, 2026