Type Inference
TypeScript can figure out a value's type from how it's written or used, without an explicit annotation. Most real-world TypeScript code relies on inference far more than explicit annotations — annotating every single variable is noisy and considered bad style once inference already gets it right.
🎯 What you'll learn
- What Inference Means
letvsconst— Widening and Literal Types- Contextual Typing — Inference Flowing Into an Expression
- Best Common Type for Arrays
- Return Type Inference
1What Inference Means
TypeScript can figure out a value's type from how it's written or used, without an explicit annotation. Most real-world TypeScript code relies on inference far more than explicit annotations — annotating every single variable is noisy and considered bad style once inference already gets it right.
let count = 5; // inferred: number
let name = "Ana"; // inferred: string
let items = [1, 2, 3]; // inferred: number[]2let vs const — Widening and Literal Types
let a = "hello"; // inferred as the WIDER type: string
const b = "hello"; // inferred as the NARROWER literal type: "hello"Because let allows reassignment, TypeScript widens the inferred type to the general string — a could later hold any string. const can never be reassigned, so TypeScript keeps the most precise type possible: the exact literal "hello".
function greet(role: "admin" | "guest") {}
const role = "admin";
greet(role); // ✅ role's type is the literal "admin", assignable to "admin" | "guest"
let mutableRole = "admin";
greet(mutableRole); // ❌ mutableRole's type widened to `string` — NOT assignable to "admin" | "guest"3Contextual Typing — Inference Flowing Into an Expression
Inference doesn't only flow outward from a value to its variable — it can flow inward from an expected type into an expression, filling in parameter types you never wrote.
const numbers = [1, 2, 3];
numbers.map((n) => n * 2);
// ^ n is inferred as `number`, WITHOUT you annotating it —
// TypeScript already knows .map's callback receives the array's element typeTypeScript knows .map's signature expects (value: number, index: number, array: number[]) => U, and propagates the number type from that expected signature into your arrow function's parameter — this is called contextual typing.
4Best Common Type for Arrays
const mixed = [1, "two", 3]; // inferred: (string | number)[]
const points = [{ x: 1 }, { x: 2, y: 3 }]; // inferred: { x: number; y?: number }[]When array elements have different (but related) types, TypeScript computes the best common type — a union broad enough to describe every element, or a merged shape for objects.
5Return Type Inference
function add(a: number, b: number) {
return a + b; // return type inferred as `number` — no explicit `: number` needed
}TypeScript infers a function's return type by analyzing every return statement in its body. For recursive functions or complex conditional logic, explicit return type annotations are still recommended — inference can be slow or ambiguous on deeply recursive code, and an explicit return type also documents intent and catches accidental changes.
6Inference Across Destructuring
function getUser() {
return { id: 1, name: "Ana", active: true };
}
const { id, name } = getUser(); // id: number, name: string — inferred from the return type7as const — Requesting Maximum Narrowing
let point = { x: 1, y: 2 }; // { x: number; y: number }
let pointConst = { x: 1, y: 2 } as const; // { readonly x: 1; readonly y: 2 }as const tells the inference engine to widen as little as possible: object properties become readonly, and literal values keep their exact literal type instead of widening to number/string. Commonly used for configuration objects, Redux-style action types, and anywhere exact literal types matter.
Under the HoodHow the Inference Algorithm Actually Decides▾
Two directions of information flow
The checker doesn't infer types in one single pass — it combines two distinct flows of information for every expression:
- Bottom-up (synthesis): compute a type FROM the expression itself — a literal
5synthesizes tonumber, an object literal synthesizes to a shape based on its properties. - Top-down (contextual typing): if the expression appears somewhere with an expected type already known (a function parameter, an assignment target, an array being pushed into a typed array), that expected type is pushed INTO the expression before synthesis even happens — this is exactly the mechanism behind section 3's
.map()callback example.
numbers.map((n) => n * 2)
▲
│ expected type "number" is pushed in from .map's own signature
│ BEFORE the arrow function's body is even checkedWithout contextual typing, n would have no way to get a type at all (there's no annotation, and nothing to synthesize it from in isolation) — TypeScript would either error or fall back to any. Contextual typing is what makes untyped callback parameters "just work" throughout the standard library and beyond.
Literal widening is a real internal type distinction
The checker actually tracks TWO kinds of types for literals internally: the precise literal type ("hello", 5, true) and its widened general form (string, number, boolean). Every literal value starts with its precise literal type; widening to the general form happens at specific, well-defined points — assigning to a let (mutable, so future values could be any string), but NOT assigning to a const (section 2's exact distinction), and NOT when a contextual type already expects the literal specifically (section 3-style propagation can keep a literal narrow even through a let, in some contexts).
Best common type uses a supertype search, not just "make a union"
For section 4's array example, the checker doesn't blindly union every element type — it first checks whether one of the candidate types is already a supertype of all the others (in which case it just uses that single type, no union needed), and only falls back to constructing a real union type when no single element type covers every other element structurally.
⚠️ Common Pitfalls
- Relying on inference for public function signatures — inferred return types can change unexpectedly when the implementation changes, silently breaking callers; explicit return types on exported/public functions are safer.
- Expecting a
letvariable to keep a literal type — it always widens; useconstor an explicit literal type/as constif you need the narrow type to persist. - Assuming contextual typing always applies — it only flows in when TypeScript can already determine an expected type from context (an annotated parameter, a typed array, an assignment target); a genuinely standalone expression has nothing to receive from.
- Overusing
as const— it makes every nested propertyreadonly, which can fight with code that legitimately needs to mutate the object later.
📋 Quick Reference
let x = "hi" → widened: string
const x = "hi" → literal: "hi"
{ x: 1 } as const → { readonly x: 1 }
callback params → often inferred via contextual typing, no annotation needed🎤 Interview Q&A
Q1How does TypeScript infer types?core▾
From initializers (let x = 3 → number), return statements, contextual typing (callback parameters inferred from the function they're passed to), and generic arguments inferred from usage.
- Should you annotate everything? No; annotate function boundaries and let inference handle locals.
Q2What is literal type widening?core▾
let x = 'a' widens to string, while const x = 'a' keeps the literal 'a'. Object properties widen too unless you use as const.
- What does as const do? Makes literals readonly and narrowest types.
Q3What is contextual typing?core▾
Types flowing from the context into an expression: in arr.map(x => x.id), x is inferred from the array's element type; event handlers infer event types.
- When does it fail? When callbacks are defined separately from where they're used.
Q4How does inference work with generics?senior▾
TS infers type parameters from arguments: identity(42) infers T = number. Inference candidates combine from multiple arguments; NoInfer<T> prevents inference from specific positions.
- Why NoInfer? To make one argument drive inference and others be checked against it.
Q5Should you annotate return types?core▾
For exported/public functions, yes: it documents the contract, speeds up type checking and catches accidental changes. For small internal functions, inference is fine.
- isolatedDeclarations? Requires explicit types on exports for faster declaration emit.
🛠️ Practice exercises
Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.