There are two ways TypeScript learns about types: we tell it (annotations) or it figures it out from the code (inference). Knowing when to use each keeps our code clean.
Annotations — we declare the type
We add : Type after a variable, parameter, or return.
const name: string = "Manish";
function add(a: number, b: number): number {
return a + b;
}
Inference — TS reads our code
If we initialize a variable, TS already knows its type. We don’t need to repeat ourselves.
const name = "Manish"; // inferred as string
const age = 27; // inferred as number
const user = { id: 1, email: "x@y.com" }; // inferred as { id: number; email: string }
In simple language, if it’s obvious from the value, just don’t annotate.
The rule of thumb
- Annotate at the boundaries — function parameters, function return types (for public APIs), and exported values.
- Let inference handle the inside — local variables, intermediate expressions.
// ✅ Good — annotate inputs/outputs, infer locals
function calculateTotal(items: number[]): number {
const sum = items.reduce((acc, n) => acc + n, 0); // 'sum' inferred as number
const tax = sum * 0.18; // inferred
return sum + tax;
}
// ❌ Noisy — over-annotated
function calculateTotalBad(items: number[]): number {
const sum: number = items.reduce((acc: number, n: number) => acc + n, 0);
const tax: number = sum * 0.18;
return sum + tax;
}
Why annotate function returns?
Even when TS can infer the return type, writing it down has two benefits:
- Catches bugs at the source. If we accidentally return the wrong thing, the error shows up in the function, not at every call site.
- It’s a contract. Other devs see the signature without reading the body.
function getUser(id: number): { id: number; name: string } {
// if we accidentally return null here, error is HERE not at the caller
return { id, name: "Manish" };
}
The let vs const widening gotcha
const of a literal infers the literal type. let widens to the primitive.
const direction = "left"; // type is "left" (literal)
let dir = "left"; // type is string (widened)
This matters when assigning to typed slots — see the Literal Types note.
Inference for arrays and objects
Empty arrays are inferred as any[] — usually a mistake. Annotate when starting empty.
const nums = []; // any[] — bad
const nums2: number[] = []; // number[] — good
Interview soundbite
“Let inference do the work. Annotate at the boundaries — function params, return types, public APIs. Avoid over-annotating locals; it’s just noise that has to be maintained.”