Type annotations and inference

TypeScript · Essentials · 10 min

Idiomatic TypeScript annotates boundaries and lets inference do the rest, which keeps code short and still safe.

You rarely need to annotate everything. TypeScript infers a type from an initializer: let count = 0 makes count a number, and const names = ["a", "b"] is string[]. Hover in your editor to see what was inferred.

Where annotations earn their keep:

  • Function parameters — they can't be inferred from the body (and without strict's noImplicitAny they silently become any).
  • Public return types of exported functions, so a change in the body can't silently change the API.
  • Variables declared without a value (let result: string;) and empty collections (const ids: number[] = []).
  • When you want a wider or narrower type than the one inferred.

Inference also flows "backwards" through contextual typing: in names.map(n => n.length) the callback parameter n is a string because map knows the array's element type.

Watch literal widening. A const binding can never change, so const mode = "dark" has the literal type "dark". A let can be reassigned, so let mode = "dark" widens to string. Object properties are mutable too, so { mode: "dark" } has mode: string. This matters when a function expects the literal type "dark" | "light" — later lessons show as const and satisfies for those cases.

A type annotation (const x: T = value) asks the checker to verify the value fits T. A type assertion (value as T) instead tells the checker to trust you and skips most checking. Prefer annotations; reach for as only when you know something the compiler can't.

Example

let count = 0;                 // number
const mode = "dark";           // "dark" (literal type)
let theme = "dark";            // string (let widens)
const settings = { mode: "dark" }; // { mode: string }

const lengths = ["ab", "cde"].map(s => s.length); // s: string, result: number[]

// Annotate parameters and exported return types:
export function average(values: number[]): number {
  return values.reduce((a, b) => a + b, 0) / values.length;
}

const ids: number[] = [];      // without the annotation this would be any[] / never[]
ids.push(count);

// Assertion = "trust me" (no conversion happens at runtime!)
const input = JSON.parse('{"n": 1}') as { n: number };
console.log(mode, theme, settings, lengths, average([1, 2]), input.n);

Remember

  • Annotate parameters, exported return types and empty values; let inference do the rest
  • const keeps literal types; let and object properties widen them
  • Annotation = checked; as assertion = trusted, not checked

Watch out for

  • Using as to silence an error rather than fixing the type — it hides real bugs.

Check yourself

  1. What is the inferred type of p in: let p = "north"?
  2. In [1, 2, 3].map(n => n * 2), why does n need no annotation?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-annotations-on-variables