In a TypeScript review you see every local variable annotated, as in `const count: number = items.length`. Is that worth flagging, and where does an explicit annotation on a variable genuinely earn its place?
answer
- does it tell the compiler anything new?
- inference cannot go stale
- any is assignable to everything
- empty arrays and null initializers
- pin the contract at the boundary
basics
~20 sMostly yes: annotating what inference already knows adds noise and can silently launder an any, because any is assignable to every type. Annotations earn their place on declarations inference cannot resolve — empty collections, uninitialised or nullish-initialised variables, and exported values.
solid answer
~50 sRestating what the compiler already infers is noise, and it drifts: when `items.length` becomes something else, the annotation goes stale rather than the code failing. The sharper argument is that an annotation is checked as an *assignability* constraint, and `any` is assignable to everything — so `const count: number = legacy.getCount()` compiles happily when `getCount()` is untyped, and the annotation makes the value *look* checked while nothing was verified. Annotations do earn their place in three cases: when the initializer cannot determine the type (`const ids: string[] = []`, `let current: Shape | null = null`), when the inferred type is not the one you want for the variable's whole life, and at module or exported boundaries, where the annotation localises an error at the definition instead of scattering it across call sites. Everywhere else, let inference do it.
code
typescript · 11 linesdeclare function getCount(): any;
// Compiles, checks nothing: any is assignable to number.
const launderedCount: number = getCount();
// Honest: the untyped value stays visible to tooling.
const rawCount = getCount(); // any
// Annotations that add real information:
const ids: string[] = [];
let current: { kind: "circle" } | null = null;go deeper
Know that TypeScript already infers a variable's type from its initializer, so repeating it is usually unnecessary. Remember the one case you must annotate: a declaration with nothing useful to infer from, like an empty array.
Explain that an annotation is an assignability check performed at compile time and erased afterwards, and that any satisfies every annotation. Name the cases where an annotation adds information the initializer does not carry.
Make the review call and justify it: distinguish harmless redundancy from an annotation masking an untyped value, and push the fix to the boundary where the data enters rather than to the annotation itself.
Set the convention: where the codebase requires explicit types (exported surfaces, module boundaries) versus where inference is preferred, and which lint rules make unsafe assignment from untyped sources visible instead of leaving it to reviewers to spot.
## The default: let inference work TypeScript infers a variable's type from its initializer. `const count = items.length` gives `count` the type `number` with no help from you, and unlike a comment the inferred type cannot go stale — it is recomputed from the code every build. Writing `const count: number = items.length` restates a fact the compiler already knows, which costs a line of reading and buys nothing. Worse, it rots. If `items.length` is later replaced by something returning a different type, the annotation does not describe the code any more, and the resulting error message points at the assignment rather than at the change that caused it. ## The hazard people miss: annotations launder `any` This is the argument that turns a style preference into a correctness one. An annotation on a variable is an **assignability check**, and `any` is assignable to every type. So this compiles: ```ts declare function getCount(): any; // untyped dependency, JSON.parse, etc. const count: number = getCount(); // no error, no check count.toFixed(2); // trusted; may explode at runtime ``` The annotation does not verify anything at runtime — it is erased — and it does not verify anything at compile time either, because the source type is `any`. What it *does* is stop the `any` from being visible downstream, so tools and readers looking for untyped values see a clean `number`. Compare with letting inference run: `const count = getCount()` gives `count` the type `any`, which is at least honest and is what a no-implicit-any style rule or a lint rule against unsafe assignment can catch. The fix is not to remove the annotation and accept the `any`. It is to convert at the boundary — validate, or write a wrapper that returns a real type — so the annotation reflects a check that actually happened. ## Where annotations genuinely earn their place **1. The initializer cannot tell the whole story.** An empty array or a nullish initializer describes only the variable's first instant: ```ts const ids: string[] = []; // otherwise the element type is unknown to you let current: Shape | null = null; // otherwise the declaration says only 'null' ``` A bare `let x;` is the extreme case: with nothing to infer from, you get an implicit `any`, and every later use is unchecked. **2. The inferred type is not the one you want.** Inference describes the initializer, not your intent. If a mutable variable will later hold a different member of a union, or if you deliberately want the widened primitive rather than the precise type the initializer produced, say so explicitly. The annotation is the tool for stating intent that the first value does not imply. **3. Boundaries you export.** A module-level or exported value is a contract. Annotating it pins the contract at the definition: if the implementation drifts, the error lands on that line. Without the annotation, the exported type silently changes shape and the errors appear scattered across every consumer — the same reasoning that makes annotated return types valuable on a public API surface. ## What a good review comment sounds like Rather than "annotations are bad", the useful review framing is a question: *does this annotation tell the compiler something it did not already know?* If yes, keep it. If no, it is either noise or — when the right-hand side is untyped — a mask, and the mask is the one worth arguing about. ## Erasure, and what that means for trust Every annotation in this discussion disappears at compile time. There is no runtime validation attached to `: number`, so the confidence a reader draws from seeing it is entirely borrowed from the compiler's check — and that check is only as strong as the type on the right-hand side. That is the whole reason `any` laundering matters: the reader's trust survives the compiler's check being vacuous. ## The short version Default to inference; it cannot rot. Annotate when inference has nothing to work with, when your intent is wider or different than the initializer implies, or when you are pinning an exported contract. Treat an annotation whose right-hand side is `any` as a defect, not as documentation.
- Why is `const total: number = JSON.parse(body).total` misleading rather than safe?`JSON.parse` returns `any`, and `any` is assignable to every type, so the annotation is accepted without checking anything — and nothing is checked at runtime either, since annotations are erased. The value may be a string or undefined and the code will still compile. The honest fix is validation at the boundary that produces a real type, with the annotation reflecting that check.
- Does the same argument apply to function return type annotations?The tradeoff flips at boundaries. On an exported function an explicit return annotation pins the contract at the definition, so an implementation drift errors there rather than at every call site — and it stops an accidental `any` from leaking outward. For small internal helpers, inference is usually fine. The rule of thumb is: annotate what other code depends on.
- What does a bare `let x;` with no initializer and no annotation give you?Nothing to infer from, so the declaration has an implicit `any` and every subsequent use goes unchecked. Under a no-implicit-any style the compiler will complain in the cases it can see. Either give it an initializer that determines the type or write the annotation that describes the variable's whole range of values.
saying these in an interview costs you the question
- Says an annotation guarantees the value has that type at runtime.
- Believes annotating a variable converts or validates an any.
- Thinks more annotations always mean more type safety.
- Claims inferred types drift out of date like comments do.
- Removes the annotation and accepts the any instead of fixing the boundary.