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:
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"→ typestring. 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:
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