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 = 42infersx: 42(literal type withconst);let x = 42infersx: 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) => ...)hasiteminferred from the array's element type.
When to add explicit annotations:
- Function parameters: TypeScript cannot infer what a caller will pass.
- Public API surfaces: Exported functions and class methods — explicit annotation makes the contract clear.
- When inference is too broad:
let status = "pending"infersstring; if you want the literal"pending", useconst status = "pending" as constorlet status: "pending" | "done". - Complex generics: When inference would produce an unwieldy inferred type.
- 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.