Type Inference vs Explicit Annotations
Let TypeScript infer types where safe, annotate at boundaries — functions, APIs, and exported modules.
Good TypeScript feels like writing JavaScript with guardrails — not like writing types twice. The compiler infers types from initializers, return expressions, and control flow. Explicit annotations belong at public boundaries: exported functions, shared package types, and anywhere inference would be too wide or too narrow for your intent.
Understanding inference saves keystrokes and keeps locals readable. Over-annotating every const adds noise reviewers ignore. Under-annotating exported APIs hides contract changes — a function body refactor can silently widen a return type unless the signature is pinned explicitly.
Variable inference from initializers
const bindings infer the narrowest type that fits the initializer. let widens because reassignment is allowed — let n = 42 is number, not literal 42. Without an initializer, let status; triggers noImplicitAny under strict settings, which is why you initialize or annotate at declaration.
Return type inference and contextual typing
Return types infer from return statements in the function body. Export signatures explicitly so public contracts stay stable when implementation changes. Contextual typing applies to callbacks in known positions — Array.map callbacks inherit element types without repeating parameter annotations.
[1, 2]When explicit annotations win
Annotate empty arrays before push — [] alone infers never[]. Annotate every function parameter in .ts files because inference has no initializer to work from. Use as const to freeze literal unions on config objects instead of widening to string.
| Situation | Recommendation |
|---|---|
| const x = 42 | Omit — inferred number |
| const x = [] | Annotate element type |
| Exported function | Explicit return type |
| .map callback | Omit param type — contextual |
satisfies without widening literals
Annotating const config: Config widens literal fields to the interface type. satisfies Config validates shape while preserving narrow literals for autocomplete and exhaustiveness downstream.
- Infer locals; annotate exports and parameters
- as const for readonly literal configs
- satisfies validates without widening
- 1const narrows; let widens
- 2Return types infer but export explicitly
- 3Empty arrays need element type
- 4Next: unions and literal types