Q6 of 38 · TypeScript

How does TypeScript's type inference work, and when should you add explicit type annotations?

TypeScriptJuniortypescripttype-inferenceas-constfundamentalsannotations

Short answer

Short answer: TypeScript infers types from initializer values, return expressions, and contextual positions. Explicit annotations are needed for function parameters, public API surfaces, complex return types, and when inference widens a type too broadly (e.g., infers `string` when you want a literal union).

Detail

Type inference is TypeScript's ability to determine a type automatically without an explicit annotation. It makes TypeScript usable without annotating everything.

How inference works:

  • Variable initialization: const x = 42 infers x: 42 (literal type with const); let x = 42 infers x: number (widens because let can be reassigned).
  • Return type: If every return path is typed, TypeScript infers the function's return type.
  • Contextual typing: A callback passed to .map((item) => ...) has item inferred from the array's element type.

When to add explicit annotations:

  1. Function parameters: TypeScript cannot infer what a caller will pass.
  2. Public API surfaces: Exported functions and class methods — explicit annotation makes the contract clear.
  3. When inference is too broad: let status = "pending" infers string; if you want the literal "pending", use const status = "pending" as const or let status: "pending" | "done".
  4. Complex generics: When inference would produce an unwieldy inferred type.
  5. Disambiguation: Two overloaded paths where TypeScript would infer a union you don't want.

const assertions: as const narrows inference to the most specific literal type: const LEVELS = ["junior", "mid", "senior"] as const infers readonly ["junior", "mid", "senior"] rather than string[].

// EXAMPLE

// Inference — no annotation needed
const name = "Alice";  // inferred: "Alice" (literal)
let score = 80;        // inferred: number (widened from let)

// Contextual inference
const names = ["Alice", "Bob"];
const upper = names.map(n => n.toUpperCase()); // n: string inferred

// Inference too broad — fix with annotation or as const
let status = "pending";     // string — too wide
const STATUS: "pending" | "active" = "pending"; // explicit
const config = { env: "staging" } as const;     // readonly { env: "staging" }

// Function params need annotation
function greet(name: string): string { // explicit
  return `Hello, ${name}`;
}

// Return type inferred
function add(a: number, b: number) { // return: number inferred
  return a + b;
}

// WHAT INTERVIEWERS LOOK FOR

The difference between const (literal) and let (widened) inference. Knowing when annotation is required vs redundant. The `as const` pattern for literal unions from arrays. Practical judgment — over-annotating is almost as bad as under-annotating.

// COMMON PITFALL

Annotating every variable explicitly — `const name: string = 'Alice'` is redundant and adds noise. Add annotations where they add information or enforce a contract, not where inference already gets it right.