Inference gives a type to anything without an annotation. The handbook lists where it happens: variable and member initializers, parameter defaults, and function return types. It runs in two directions.
- Bottom-up. The type comes from the expression.
let n = 3isnumber;const c = 3is the literal3. For[0, 1, null]the checker takes the candidate types and picks the one compatible with the others, or falls back to a union:(number | null)[]. It does not invent a shared supertype, so[new Rhino(), new Elephant()]is an array of the union, notAnimal[]. - Top-down (contextual typing). The type comes from the position. The parameter of a callback passed to
[1, 2, 3].map(...)isnumberbecausemapdeclares it. The contextual type also keeps literals alive, sodraw({ kind: "circle" })checks whendrawexpects"circle".
Widening is the rule that surprises people. A mutable location (let, an object property) could be reassigned, so a literal initializer widens to its base type: { kind: "circle" } stored in a variable has kind: string, and passing that variable to draw fails. A const binding keeps the literal; as const freezes a whole object literal.
Where to annotate:
- Exported functions and public APIs: annotate the return type so a change in the body can't silently change the contract (and
isolatedDeclarations, TS 5.5, requires it). - Unannotated parameters with no contextual type: they are
any, whichnoImplicitAny(part ofstrict) reports. - Empty collections and
new Map()with no arguments: there is nothing to infer from, so you getany-flavored types. - When you want a wider type than the initializer gives (
const xs: Animal[] = [...]).
Elsewhere, leave annotations off. Local variables and callbacks read better, and stay correct under refactoring, when inferred.