Type Annotations vs Type Inference

beginner annotations inference best-practices

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:

  1. Catches bugs at the source. If we accidentally return the wrong thing, the error shows up in the function, not at every call site.
  2. 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.”