Annotations & Inference

You can tell TypeScript the type of a value explicitly with an annotation, or let it work the type out from the code — that's inference. Good TypeScript uses both: annotations where they document intent, inference everywhere else.

Explicit annotations

An annotation is the : type you write after a name. It's a promise: from now on, only values of that type may live here.

TS explicit.ts
let city: string = "Lisbon";
let population: number = 545_000;

function area(width: number, height: number): number {
  return width * height;
}

console.log(city, population, area(3, 4));
Output
Lisbon 545000 12

Inference

Writing : string next to a string literal is repetitive. When a variable is initialised, TypeScript infers its type from the value:

TS inference.ts
let city = "Lisbon";
city = 42;
tsc output
error TS2322: Type 'number' is not assignable to type 'string'.

There's no annotation, yet city is a string — exactly as if you'd written one. Inference isn't "less typed"; it's the same type checking with less noise.

Inference also works for function return values, array elements and object properties:

TS more-inference.ts
function double(n: number) {
  return n * 2;
}

const result = double(21);
const tags = ["new", "sale"];
const user = { name: "Ana", active: true };

console.log(typeof result, typeof tags[0], typeof user.active);
Output
number string boolean

Hover over double in an editor and you'll see function double(n: number): number — the return type was inferred from the return statement.

let vs const inference

How specific the inferred type is depends on whether the variable can change:

TS widening.ts
let mutable = "draft";
const fixed = "draft";

// mutable is inferred as: string
// fixed   is inferred as: "draft"
mutable = "published";
console.log(mutable, fixed);
Output
published draft

A let can be reassigned later, so TypeScript widens it to the general type string. A const can never change, so its type is the exact literal type "draft". Literal types turn out to be extremely useful — lesson 11 is dedicated to them.

Where inference can't help

Inference needs something to infer from. Two situations where you must annotate:

Function parameters

A parameter has no value until someone calls the function, so TypeScript can't guess it. Without an annotation (and with strict mode on) you get an error:

TS params.ts
function shout(text) {
  return text.toUpperCase();
}
tsc output
error TS7006: Parameter 'text' implicitly has an 'any' type.

Variables declared before they're assigned

TS later.ts
let status: string;

if (Math.random() > 0.5) {
  status = "heads";
} else {
  status = "tails";
}
console.log(status.length);
Output
5

Without : string, status would start life with no information to infer from. TypeScript also checks that it's definitely assigned on every path before it's read.

When to annotate anyway

  • Function parameters — always required.
  • Exported function return types — optional, but writing them means a mistake inside the function is reported at the function, not at some distant caller.
  • Empty arrays and objects — const ids = [] tells TypeScript nothing about future contents; write const ids: number[] = [].
  • Everything else — let inference do it. const total: number = 0 adds nothing that const total = 0 doesn't already say.

Try it yourself

Each variable below is inferred. Predict what typeof will print before you run it:

Try it yourself
Console output