Menu

TypeScript Type Inference: When to Annotate and When Not

TypeScript works out most types from the values you write. Learn what it infers for variables, let vs const, objects, arrays and return values, how callbacks get their types from context, and where you still need an annotation.

This page includes runnable editors - edit, run, and see output instantly.

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

SituationAnnotate?
Variable initialized with a valueNo, the value gives the type
Variable declared without a value (let result;)Yes, let result: string;
Function parametersYes (except callbacks with context, or a default value)
Return type of a small internal functionOptional
Return type of an exported or public functionRecommended, as a contract
Empty array or object you fill laterYes, const ids: number[] = []
Value should keep its literal typesUse as const
Value must match a type but keep its own inferred typeUse 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.

Coddy programming languages illustration

Learn to code with Coddy

GET STARTED