Type Inference
You write fewer types than you think
Section titled “You write fewer types than you think”Good TypeScript is not covered in annotations. The compiler infers most types, and fighting it with redundant annotations is a beginner tell. The skill is knowing what it infers and where you actually need to help.
let count = 5; // inferred: numberconst name = "Ada"; // inferred: "Ada" (a literal type!)const items = [1, 2]; // inferred: number[]Note that count and name are inferred differently, and that difference is one of the most important rules in the language.
let widens, const keeps the literal
Section titled “let widens, const keeps the literal”A const can never be reassigned, so the compiler keeps its narrowest type — the literal. A let can be reassigned, so the compiler widens to the general type.
const a = "hello"; // type: "hello" (literal — it can't change)let b = "hello"; // type: string (widened — b might become any string)This is why literal types “appear and disappear.” It matters constantly:
const method = "GET"; // type: "GET"let method2 = "GET"; // type: string
function req(m: "GET" | "POST") {}req(method); // ✅ "GET" fits the unionreq(method2); // ❌ string is too wide for "GET" | "POST"flowchart LR lit["literal value "GET""] -->|const: can't change| keep["type: "GET" (narrow)"] lit -->|let: might change| wide["type: string (widened)"]
When you want a literal type from a let or an object property, use a const assertion (as const) — a whole topic in the next module.
Contextual typing: inference flows inward
Section titled “Contextual typing: inference flows inward”Inference isn’t only bottom-up (from a value to its type). It also flows top-down from the surrounding context — this is contextual typing.
const nums = [1, 2, 3];
nums.forEach((n) => { // n is inferred as number — you didn't annotate it. // The context (an array of number) told the compiler what n must be. console.log(n.toFixed(2));});
window.addEventListener("click", (e) => { // e is inferred as MouseEvent from the event name — pure contextual typing.});This is why callback parameters usually don’t need annotations: the function you’re passing them to already declares their types, and inference flows in.
Best common type
Section titled “Best common type”When a value could be several types — like an array with mixed elements — the compiler computes a best common type that covers them all:
const mixed = [1, "two", 3]; // inferred: (string | number)[]const shapes = [circle, square]; // inferred: (Circle | Square)[]It doesn’t pick one and error on the rest; it forms the union that fits every element. Occasionally the best common type is wider than you want, which is a signal to annotate explicitly.
When to annotate
Section titled “When to annotate”Let inference do the work, and annotate deliberately in three places:
- Function parameters — inference can’t read your mind about inputs (though contextual typing covers callbacks).
- Function return types on public APIs — annotating the return type catches mistakes inside the function and documents intent. (Inference works, but an explicit return type is a guardrail.)
- When inference is wider than you want — e.g. you need a literal or a specific union, not the widened type.
Everywhere else, prefer inference. Redundant annotations are noise that can drift from reality during refactors.