Type Inference and Annotations
Introduction
You do not need to label every variable in TypeScript. The compiler infers types from initial values and control flow, and you add explicit annotations when inference is too wide, unclear, or impossible. This chapter explains when to rely on inference, when to write types yourself, and how to avoid fighting the checker.
Prerequisites
- Basic Types in TypeScript
strict: truein yourtsconfig.json
Inference vs Annotation
| Approach | Syntax | Who decides the type |
|---|---|---|
| Inference | const x = 10 | TypeScript |
| Annotation | const x: number = 10 | You (checked by TypeScript) |
Both are valid. Good TypeScript code uses inference where it is clear and annotations where it helps readers or fixes a bug.
Variable Inference
const count = 10;
const label = "active";
const enabled = false;Inferred types:
count→numberlabel→stringenabled→boolean
Code explanation:
constwith a literal initializer gets a narrow inferred type- You do not need
: numberoncountunless your team style guide requires it
When Inference Narrows Literals
const port = 3000;
const theme = "dark";Here port is type 3000 and theme is type "dark" (literal types), not just number and string.
Widen intentionally when you need a general type:
let port: number = 3000;
let theme: string = "dark";Or use a type assertion in advanced cases—prefer annotations for clarity when learning.
let vs const
let counter = 0;
counter = 1;
const max = 100;
// max = 101; // Error: cannot assign to 'max'Code explanation:
let counteris inferred asnumberand can be reassignedconst maxis inferred as literal100ornumberdepending on value; reassignment is forbidden
Function Return Inference
function double(n: number) {
return n * 2;
}
const result = double(5);double’s return type is inferred as number. result is also number.
Add an explicit return type when the function is public API or logic is complex:
function parsePort(input: string): number {
const value = Number(input);
if (Number.isNaN(value)) {
return 3000;
}
return value;
}Code explanation:
- Parameter types are often required when there is no initializer (unlike variables)
- Return type annotation documents the contract even if inference would succeed
When You Must Write Annotations
Function parameters
function greet(name: string) {
return `Hello, ${name}`;
}Without name: string, name would be any under noImplicitAny (included in strict).
Uninitialized variables
let status: string;
status = "ready";let status alone is an error—you must annotate or assign immediately.
Empty arrays waiting for data
const items: string[] = [];
items.push("first");Without : string[], TypeScript infers never[] and rejects push("first").
Callback parameters in some contexts
const names = ["Ada", "Lin"];
const lengths = names.map((name: string) => name.length);Often inference works without annotating name; annotate when the callback is untyped and defaults to any.
Public API surfaces
Exported functions, class methods, and library types benefit from explicit parameter and return types so refactors do not silently widen contracts.
When Inference Is Enough
Prefer inference for:
- Local variables with obvious initializers (
const total = price * qty) - Private helpers where the body is short and visible
- Generic call sites where types flow from arguments (generics chapter later)
const user = { id: 1, name: "Sam" };
const id = user.id;user is inferred as { id: number; name: string }; id is number.
Contextual Typing
TypeScript uses context to infer types in assignments and callbacks:
window.addEventListener("click", (event) => {
console.log(event.type);
});event is inferred as MouseEvent because addEventListener expects that callback shape—no need to write event: MouseEvent unless you want to.
Another example:
type Point = { x: number; y: number };
function draw(point: Point) {
console.log(point.x, point.y);
}
draw({ x: 0, y: 0 });The object literal is checked against Point (excess property checking applies in direct calls).
Best Practices
- Annotate function boundaries (exported functions, public methods) with parameter and return types.
- Let locals infer when the right-hand side is self-documenting.
- Do not annotate twice—avoid
const x: number = 10unless style requires it or inference is wrong. - Fix
anyat the source instead of annotating downstream. - Use
satisfies(TypeScript 4.9+) when you want inference plus constraint—covered in advanced material; for now, prefer clear annotations on tricky objects.
Tip
Read Errors as Hints
If inference picks string but you need "a" | "b", either annotate the union or use as const on the value. The compiler tells you when inferred types are too wide.
Common Mistakes
| Mistake | What happens | Fix |
|---|---|---|
let data; | Implicit any error | Add type or initializer |
const list = [] | never[] | const list: Item[] = [] |
| Widening literal unions accidentally | Lost exhaustiveness | Annotate union or use as const |
| Over-annotating every local | Noisy code | Remove redundant : type on obvious const |
Example: Balance in One File
Code explanation:
- Export gets clear contract types
price,currency,displayinfer from values andformatPrice’s return typehistoryneedsstring[]because it starts empty
FAQ
Is explicit typing “more professional”?
Not always. Idiomatic TypeScript uses inference for locals and explicit types at module boundaries. Consistency matters more than annotating everything.
Why does TypeScript infer any for my parameter?
Usually noImplicitAny is off, or the parameter has no type in an unchecked JS file. Enable strict and add parameter types.
Can I turn off inference?
You cannot disable inference globally. You override it per site with annotations, as assertions (use sparingly), or satisfies.
What is the best type for JSON.parse result?
unknown, then narrow. Do not use any unless you accept the safety cost.
Should I annotate return types on every function?
Many teams require it on exported functions only. Internal one-liners often omit return types when inference is obvious.
Does inference work across files?
Yes, when types are exported and imported. The compiler follows imports through your project graph.