TypeScript does not require you to annotate every variable. In most code the compiler works out the type from the value you assign, the function you call, or the place an expression is used. After this lesson you will know exactly when TypeScript infers a type, when it widens a literal, how contextual typing types callbacks, and where an explicit annotation is still the right choice.
When a variable is declared with an initializer, its type is inferred from that initializer. The variable keeps that type for its entire life:
let count = 10; // number
let title = "Report"; // string
let ready = false; // boolean
let scores = [90, 85]; // number[]
count = "ten"; // Error: Type 'string' is not assignable to type 'number'Hover over a variable in VS Code and the editor shows the inferred type. Writing let count: number = 10 is legal but redundant; teams usually leave it out. When an array holds mixed values, the element type becomes a union: [1, "two", null] is (string | number | null)[].
A variable declared without an initializer is typed any unless you annotate it, and noImplicitAny (part of strict) will report it. Always give such variables an explicit type.
const and let infer differently for primitive values:
const status = "active"; // type: "active" (a string literal type)
let mode = "dark"; // type: string (widened)Because a const can never be reassigned, TypeScript keeps the exact literal type. A let may be reassigned, so the literal is widened to its base type. The same rule applies to numbers and booleans.
The as const assertion stops widening inside objects and arrays and marks every property readonly:
const roles = ["admin", "editor", "viewer"] as const;
// type: readonly ["admin", "editor", "viewer"]
type Role = (typeof roles)[number]; // "admin" | "editor" | "viewer"This pattern is the standard way to derive a union type from a runtime list, and you will use it constantly.
Function return types are inferred from the return statements. A function with several returns gets a union:
function parsePort(raw: string) {
const n = Number(raw);
if (Number.isNaN(n)) return null;
return n;
}
// inferred: (raw: string) => number | nullInference here is convenient, but exported or public functions benefit from explicit return types: they document intent and stop an accidental change in the body from silently changing the API.
Inference also flows into expressions from their surroundings. Callback parameters are typed from the function they are passed to, so you rarely annotate them:
const users = [{ name: "Ada", age: 36 }, { name: "Linus", age: 55 }];
const adults = users.filter((u) => u.age >= 18);
// u is inferred as { name: string; age: number }
window.addEventListener("click", (event) => {
console.log(event.clientX); // event is MouseEvent
});This is called contextual typing. It also applies to object literals assigned to a typed variable.
satisfies: Check Without WideningSometimes you want to verify that a value matches a type but keep the more precise inferred type. The satisfies operator does exactly that:
type Theme = Record<string, string | [number, number, number]>;
const palette = {
primary: "#0055ff",
accent: [255, 128, 0],
} satisfies Theme;
palette.primary.toUpperCase(); // OK: still known to be a string
palette.accent[0]; // OK: still known to be a tupleWith a plain annotation (const palette: Theme = ...) both properties would collapse to string | [number, number, number] and the method calls would fail.
| Situation | Recommendation |
|---|---|
| Local variable with an initializer | Let inference work |
| Function parameters | Always annotate (no inference source) |
| Exported / public function return types | Annotate for stability |
| Empty array literal [] | Annotate, otherwise it starts as any[] |
| Variables assigned later | Annotate at declaration |
| Object that must match a shape but keep detail | Use satisfies |
const list = [] and pushing later. Write const list: string[] = [] instead.let x = "a" to be the literal "a". Use const or as const when the literal matters.What is the inferred type of `const kind = "circle";`?
return statements.const keeps literal types; let widens them. as const freezes literals inside objects and arrays.satisfies checks a value against a type without losing the precise inferred type.Next lesson: Functions and Type Annotations — parameter types, optional and default parameters, and typing function values.