Type inference is TypeScript working out a type you did not write. Initialize a variable and it takes the type of the value; return a value from a function and the return type follows. Most TypeScript code carries far fewer annotations than people expect.
The comments show what an editor displays when you hover each name. The last line is a compile error (TS2322) even though count was never annotated; // @ts-expect-error marks it as expected so the rest of the block runs.
let vs const: Literal Widening
A const can never change, so TypeScript gives it the narrowest type: the literal value itself. A let gets the wider type, because it may be reassigned later.
let a = "left"; // string
const b = "left"; // "left"
let c = 42; // number
const d = 42; // 42
const e = true; // true
This matters when a function accepts only certain values:
index.ts(7,6): error TS2345: Argument of type 'string' is not assignable to parameter of type '"left" | "right"'.
Fix it with const, or by annotating the variable: let dir: "left" | "right" = "left". Literal types have their own page, literal types.
Objects and Arrays Widen Their Members
Properties of an object literal are mutable, so they widen like let, even when the object itself is const. Array elements do the same. as const makes everything readonly and keeps the literal types.
Return Type Inference
A function's return type is inferred from its return statements. When there are several, the result is the union of them.
Inferred return types are fine for most functions. Write the return type explicitly on exported or public functions when you want the signature to be a contract: a change inside the body that returns a different type then fails at the function, not at some distant caller.
Contextual Typing: Callbacks Get Their Types
Parameters are not inferred from how a function is called. There are two exceptions. A parameter with a default value takes the default's type (function retry(times = 3) makes times a number). And a function written where a function type is already expected gets its parameter types from that context, so callback parameters need no annotation.
The same applies to forEach, filter, reduce, Promise callbacks, event handlers and any function you pass to a parameter with a declared function type.
Parameters Need Annotations
Without a default value or a context like that, TypeScript does not look at how a function is called to guess its parameter types. With strict on, an unannotated parameter is error TS7006:
index.ts(2,17): error TS7006: Parameter 'x' implicitly has an 'any' type.
Write function double(x: number) and the block prints 42, with the return type number inferred from x * 2.
Best Common Type
For an array literal with different kinds of values, TypeScript infers an array of the union of their types. It does not look for a shared base type you did not mention.
An empty array is the one case to watch. const list = [] starts as an "evolving" array whose type grows with each push; an annotation, const list: string[] = [], states the intent up front and is clearer.
When to Annotate
| Situation | Annotate? |
|---|---|
| Variable initialized with a value | No, the value gives the type |
Variable declared without a value (let result;) | Yes, let result: string; |
| Function parameters | Yes (except callbacks with context, or a default value) |
| Return type of a small internal function | Optional |
| Return type of an exported or public function | Recommended, as a contract |
| Empty array or object you fill later | Yes, const ids: number[] = [] |
| Value should keep its literal types | Use as const |
| Value must match a type but keep its own inferred type | Use satisfies |
The quickest way to see what was inferred is to hover the name in your editor. Without an editor, assign the value to a variable of a wrong type, such as const probe: boolean = mixed;, and the compiler's error message names the inferred type.
Frequently Asked Questions
What is type inference in TypeScript?
It is the compiler working out a type you did not write. let count = 0 gives count the type number, and a function that returns a + b on two numbers gets the return type number. The inferred type is checked exactly like a written one.
Should I annotate every variable in TypeScript?
No. Annotate function parameters (they are not inferred from calls, only from a default value or a callback context), exported function return types if you want a stable API, and variables declared without a value. Let TypeScript infer the rest: const name: string = "Ada" repeats what the compiler already knows.
Why does const infer a literal type but let infers string?
A const can never be reassigned, so const dir = "left" gets the literal type "left". A let could later hold any other string, so let dir = "left" is widened to string. The same widening happens to properties of an object literal, because properties can be reassigned; as const stops it.
Why does TypeScript say a parameter implicitly has an 'any' type?
That is error TS7006 from noImplicitAny, part of strict. TypeScript does not infer parameter types from how a function is called, so a bare parameter would be any. Add an annotation, function double(x: number), or pass the function where its type is known from context, such as a callback to map.
How can I see the type TypeScript inferred?
Hover the name in your editor (VS Code and most editors show the inferred type in a tooltip). In code, assigning the value to a variable of an obviously wrong type makes the compiler print the inferred type in its error message.