Topic 1.3
Type Inference
In one line
Inference means TypeScript works out a type from the code, so you usually don't write it. Knowing the rules (let widens, const stays exact, arrays join their element types) tells you when to add an annotation yourself.
Think of it like this
You hand a shop assistant a bag of oranges and ask for the total price. They don't need a label that says "these are oranges": they look at what is in the bag. Inference is TypeScript looking in the bag. If you write let city = "Pune", it sees a piece of text and gives city the type string without being told.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Inference
- TypeScript working out a type itself from the code, instead of you writing it.
- Widening
- Replacing an exact value type with its wider family:
"Pune"becomesstringwhen aletis created. - Literal type
- A type with exactly one value, like
2026or"dark". - Union
- A type written
A | Bmeaning "either A or B". - Implicit any
- A variable or parameter that ended up as
anyonly because nobody gave it a type. - Contextual typing
- A type taken from the place where something is used, e.g. a callback's parameter types come from the function it is passed to.
- Signature
- The first line of a function: its name, parameter types and return type.
- Hover
- Holding the mouse over a name in the editor to see its type.
// ^?does the same in these examples.
Step by step
01Look in the bag
Write a value and TypeScript works out its type. Nothing was annotated below, but every variable has a type.
Use // ^? or hover to see it. Make checking inferred types a habit: it is how you learn what the compiler thinks, and the fastest way to understand an error.
let city = "Pune"; // string
const year = 2026; // 2026
let flag = true; // boolean
const same = true; // true02let widens, const stays exact
A const cannot change, so the exact value is the best type. A let could hold another value later, so TypeScript picks the whole family.
That is why city = "Delhi" works and city = 5 gives TS2322. Copying a const into a let widens it too: let copy = hello is string, not "hi".
03Arrays and objects: the pieces decide
The array type is the union of its parts. Add a string to a list of numbers and the whole array type changes.
Object properties are always widened, because properties can be changed later.
const nums = [1, 2, 3]; // number[]
const mixed = [1, "two", 3]; // (string | number)[]
const person = { name: "Asha", age: 30 };
// { name: string; age: number }04Return types are inferred from the returns
TypeScript reads every return and joins their types. One return a + b of two numbers gives number.
Two returns of different kinds give a union. If the union surprises you, that is useful: it often means your function has a branch you forgot about.
05Variables that start empty
const loose = [] has nothing to infer from. In strict mode TypeScript watches each push and widens the array as you go (this feature is called an evolving array type): after pushing a number and then a string, a later read of loose is (string | number)[].
It only works while the variable is used in the same function: if it is read inside a nested function, the checker cannot follow it and reports TS7034 (on the declaration) and TS7005 (on the read). The honest fix is to write the type once: const loose: (string | number)[] = [].
const loose = [];
loose.push(1);
loose.push("two");
const view = loose; // (string | number)[]
console.log(view); // [ 1, 'two' ]06A decision list: annotate or not?
Annotate: parameters, empty collections, variables declared without a value, public function return types, and places where inference widens too much.
Let inference work: simple initial values, results of function calls, callbacks passed to map, filter, addEventListener and similar.
Try it yourself
- 1
Flip let and const
In the first example change
const year = 2026;tolet year = 2026;. Predict the new type, then check. It isnumber: widened.Now change
let citytoconst city. The assignmentcity = "Delhi"itself becomes an error (TS2588: Cannot assign to 'city' because it is a constant). - 2
Predict the array
In the second example add
const flags = [true, 1, null];with a// ^?line under it. Write down your answer first: it is the union of all three kinds,(number | boolean | null)[]. - 3
Repair the config
In the third example, change
const cfg = { mode: "dark" };toconst cfg: { mode: Mode } = { mode: "dark" };. The error disappears. Then tryas constat the end of the original line instead.
Code & diagrams
Predict every answer in the Types panel before you read it. The error on city = 5 exists only because let city was widened to string, not to a number or to the exact text Pune.
Console
(nothing was printed)Expected console output
5 2026 true true hiThe object type is printed over several lines, but it is the same { name: string; age: number } shape. The return type of pick is a union of two literals because each branch returns a different exact value.
Console
(nothing was printed)Expected console output
[ 1, 2, 3 ] [ 1, 'two', 3 ] { name: 'Asha', age: 30 } 3 yesdirect is a const, so it keeps the exact value dark and fits Mode. But the property mode inside cfg is widened to string. Fix it by writing const cfg: { mode: Mode } = ... or const cfg = { mode: "dark" } as const.
Console
(nothing was printed)Expected console output
mode:dark
mode:darkBreak it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Re-assign a different kind to a let
In the first example, the line city = 5; gives the widened string variable a number.
Break #2
Pass an object property where a literal union is expected
In the third example, apply(cfg.mode) passes the widened string.
Myth vs fact
Myth
If I don't write a type, the variable is any.
Fact
Almost always TypeScript infers a precise type from the value. Only a missing starting value with no usable hints leads to any, and strict mode warns you.
Myth
Inference is a weaker version of annotations.
Fact
It is the same type system. An inferred type is as binding as a written one, and often more precise (an exact literal rather than a broad string).
Myth
You should annotate everything for safety.
Fact
Redundant annotations add no safety and can even hide precision. Annotate at boundaries (parameters, public returns) and trust inference inside.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Inference works in two directions. Bottom-up (the type of an expression from its parts) is what this topic covers. Contextual (top-down, from the expected type) fills in callback parameters and object literal members. When both disagree, the annotation wins. Topic 16.2 goes through candidates and priorities.
- ▸
The union for an array literal is built by taking each element's widened type and removing duplicates (a form of the old "best common type" algorithm, which in TypeScript means "the union"). Inside a union, a
trueand afalsetogether collapse intoboolean, which is itself the uniontrue | false. - ▸
constdeclarations use literal freshness: the exact type is fresh until it is stored in a mutable place, which is when widening happens. That is also whylet copy = hellowidens even thoughhellois a const. - ▸
Explicit return types can help compilation in large projects and are required on exported functions by
isolatedDeclarations(Topic 11.3); without one, the compiler must look through the whole function body to find the type.
Remember this
- 1
The problem this solves: If you had to write a type on every single name, code would be slow to write and cluttered to read. Most people would stop using types. Type inference is the checker working out a type from how a value is made or used. It happens everywhere: variables take the type of their starting value, a function's return type comes from its
returnstatements, and an array's type comes from what is inside it. The Types panel's// ^?lines show exactly what was inferred, and the editor shows the same when you hover. - 2
**
letandconstinfer differently.** Aconstcan never be re-assigned, so TypeScript keeps the exact value as the type:const year = 2026is of type2026(a literal type, Topic 1.2). Aletcan change later, so TypeScript widens the type to the general kind:let city = "Pune"isstring, which is whycity = "Delhi"is fine butcity = 5is an error. Widening means replacing an exact value type with its wider family ("Pune"becomesstring,truebecomesboolean). - 3
Arrays join the types of what is inside.
[1, 2, 3]isnumber[].[1, "two", 3]is(string | number)[]: an array whose every item is a string or a number. The|means "or" and is called a union (Topic 4.1). TypeScript takes the union of all the element types as the best type that fits every item, and it does not guess a single "common parent" because primitives don't have one. - 4
Objects infer property by property, and properties are always widened (they can be changed):
{ name: "Asha", age: 30 }becomes{ name: string; age: number }. That has a classic trap. Aconst cfg = { mode: "dark" }hasmode: string, so passingcfg.modeto a function that only accepts"dark" | "light"fails, even though you can see it is"dark". The fix is to annotate, or to freeze the exact value withas const(Topic 4.2). - 5
Return types are inferred too.
function add(a: number, b: number) { return a + b; }isnumberwith no annotation. A function that returns"yes"in one branch and0in another is inferred as"yes" | 0: TypeScript keeps the literals when it returns them from a function, to give callers the most information (pickin the second example below). The exception is a single literal:() => 10is inferred as returningnumber, not10, because a lone literal return is widened. If you want something less specific, or want the compiler to check your intention, write the return annotation. - 6
When to annotate, in one rule: annotate where TypeScript can't know or where you want a promise. That means function parameters, empty arrays and objects (
const names: string[] = []), variables declared without a value (let total: number;), and the public functions other code depends on. Skip annotations on simple starting values and on callbacks whose parameter types are known from context (Topic 2.4). Inference that is correct is free documentation, and an annotation that repeats it is just noise. - 7
Inference cannot read your mind.
const empty = []is anany[]waiting to be filled, andlet later;isany: a variable with no type and no value tells TypeScript nothing. WithnoImplicitAnyon (the default in strict mode) the checker watches how you use them and sometimes works it out (below), and reports an error when it can't. Giving them a type up front is clearer.
Explain it without notes
What is type inference and where does TypeScript use it?
Why is const x = 5 of type 5 but let x = 5 of type number?
What is the type of [1, "a", true], and why?
When should you add an annotation even though inference would work?
Why does passing cfg.mode fail when cfg is const cfg = { mode: "dark" }?
Practice
Without running anything, write down the inferred type of each variable. Then check with // ^?.
``ts
const a = 10;
let b = 10;
const c = [10, "ten"];
const d = { id: 10 };
const e = () => 10;
This code fails. Fix it by adding annotations, without changing any value, and say which error codes tsc gives.
``ts
let names = [];
function add(n) { names.push(n); }
function all() { return names; }
Trade-offs
- ↔
Relying on inference keeps code short and always in sync with values, but an important type can hide inside the body. For public functions, a written return type is a small cost that locks the contract.
- ↔
Exact literal inference on
constis precise but surprises people when the value later flows into a mutable place and widens. When a type matters, write it down once.
Done when you can
I can predict the inferred type of a
let, aconst, an array and an object literal.I can explain widening in one sentence.
I can read an inferred type with
// ^?or hover.I can name four situations where I should write an annotation.
I can fix the
const cfg = { mode: "dark" }trap in two ways.