Annotations & Inference

Beginner · 18 min read · ▶ live playground · ✦ checkpoint

There are two ways for a variable to get a type: you declare it, or TypeScript figures it out. Beginners over-use the first; professionals lean hard on the second. This lesson teaches the annotation syntax, shows why unannotated code is just as typed, and hands you the rule that decides which to use — one you'll apply on every line of TypeScript you ever write.

The annotation syntax

An annotation is a colon and a type after a name — a promise the checker will hold you to:

let port: number = 3000;
const host: string = "localhost";
let secure: boolean = false;
 
console.log(`${host}:${port} (secure: ${secure})`);

→ localhost:3000 (secure: false)

Once port is promised to be a number, every future assignment is checked against that promise — port = "3000" is error TS2322: Type 'string' is not assignable to type 'number', caught at your desk, not in production.

Inference — the checker already knows

Now delete the annotations. Nothing about the safety changes:

typescript — playgroundtype-checked
⌘/Ctrl + Enter to run

let port = 3000 gives port the type number by inference: the checker reads the initializer and works the type out itself. Inferred types are not weaker, fuzzier, or optional — they're the exact same types, enforced the exact same way, as the break-it line proves.

Try the > prompt below the playground: type port * 2 (fine), then port.toUpperCase(). The REPL is type-checked too — you'll get the real TS2339 — because on this site, everything goes through the checker first.

Here's the rule in one function — two annotations at the boundary, zero in the middle:

function describeOrder(items: string[], unitPrice: number): string {
  const count = items.length;        // inferred: number
  const total = count * unitPrice;   // inferred: number
  const label = items.join(", ");    // inferred: string
  return `${count} items (${label}) — $${total}`;
}
 
console.log(describeOrder(["pen", "pad"], 4));

→ 2 items (pen, pad) — $8

const vs let — your first literal types

Look back at the playground comment: const host = "localhost" inferred not string but "localhost" — a literal type, a set containing exactly one value. Inference deliberately differs between the two keywords:

  • let city = "Oslo" → type string. It can be reassigned, so any string must be allowed.
  • const city = "Oslo" → type "Oslo". It can never be reassigned, so the checker keeps the most precise fact available.

This looks like trivia for about two more sections — then it becomes the foundation of the course's most powerful patterns. A value of type "free" | "pro" (lesson 4.1) is only expressible because single string values can be types. You met one in 1.1's playground without knowing its name.

Hover — watching inference think

In VS Code (from 1.4), hover any name and a tooltip shows its type — annotated or inferred, the checker doesn't distinguish. This is the single most-used TypeScript skill: when a type error confuses you, hover the pieces and find where reality diverged from your mental model.

On this site, the > REPL prompt is your hover. A statement echoes its value; and you can ask type-layer questions directly — try typing a type alias at the prompt after running the playground below:

typescript — playgroundtype-checked
⌘/Ctrl + Enter to run

Inference tracked number[] through map, into decades, then switched to string at join — hover each name in your editor and you'll see every step. At the > prompt, try type Year = number; — it echoes undefined (types produce nothing at runtime, as hello.js taught you), but the alias persists: const y: Year = 2026 now works at the prompt, and const bad: Year = "soon" earns a real diagnostic.

Next lesson: the primitive types themselves — including the two different "nothing" values and why the capital-letter String type is a trap.

Checkpoint

Answer all three to mark this lesson complete

+50 XP on completion