Command Palette

Search for a command to run...

Hectal
PHASE 1Beginner ~22 min· topic 3 of 7

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" becomes string when a let is created.
Literal type
A type with exactly one value, like 2026 or "dark".
Union
A type written A | B meaning "either A or B".
Implicit any
A variable or parameter that ended up as any only 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.

inferred.tswhole filets
let city = "Pune";      // string
const year = 2026;      // 2026
let flag = true;        // boolean
const same = true;      // true

02let 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".

let widens, const stays exactdiagram
Rendering diagram…

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.

arrays-objects.tswhole filets
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)[] = [].

evolving.tswhole filets
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. 1

    Flip let and const

    In the first example change const year = 2026; to let year = 2026;. Predict the new type, then check. It is number: widened.

    Now change let city to const city. The assignment city = "Delhi" itself becomes an error (TS2588: Cannot assign to 'city' because it is a constant).

  2. 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. 3

    Repair the config

    In the third example, change const cfg = { mode: "dark" }; to const cfg: { mode: Mode } = { mode: "dark" };. The error disappears. Then try as const at the end of the original line instead.

Code & diagrams

let widens, const keeps the exact value

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 hi
What arrays, objects and functions infer

The 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 yes
When inference is too wide: the const object trap

direct 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:dark

Break 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.

terminal
$ Types panel
── what you'll see ──
index.ts(10,1): error TS2322: Type 'number' is not assignable to type 'string'.

Break #2

Pass an object property where a literal union is expected

In the third example, apply(cfg.mode) passes the widened string.

terminal
$ Types panel
── what you'll see ──
index.ts(11,19): error TS2345: Argument of type 'string' is not assignable to parameter of type 'Mode'.

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 true and a false together collapse into boolean, which is itself the union true | false.

  • ▸

    const declarations use literal freshness: the exact type is fresh until it is stored in a mutable place, which is when widening happens. That is also why let copy = hello widens even though hello is 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. 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 return statements, 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. 2

    **let and const infer differently.** A const can never be re-assigned, so TypeScript keeps the exact value as the type: const year = 2026 is of type 2026 (a literal type, Topic 1.2). A let can change later, so TypeScript widens the type to the general kind: let city = "Pune" is string, which is why city = "Delhi" is fine but city = 5 is an error. Widening means replacing an exact value type with its wider family ("Pune" becomes string, true becomes boolean).

  3. 3

    Arrays join the types of what is inside. [1, 2, 3] is number[]. [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. 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. A const cfg = { mode: "dark" } has mode: string, so passing cfg.mode to 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 with as const (Topic 4.2).

  5. 5

    Return types are inferred too. function add(a: number, b: number) { return a + b; } is number with no annotation. A function that returns "yes" in one branch and 0 in another is inferred as "yes" | 0: TypeScript keeps the literals when it returns them from a function, to give callers the most information (pick in the second example below). The exception is a single literal: () => 10 is inferred as returning number, not 10, 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. 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. 7

    Inference cannot read your mind. const empty = [] is an any[] waiting to be filled, and let later; is any: a variable with no type and no value tells TypeScript nothing. With noImplicitAny on (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

01

What is type inference and where does TypeScript use it?

02

Why is const x = 5 of type 5 but let x = 5 of type number?

03

What is the type of [1, "a", true], and why?

04

When should you add an annotation even though inference would work?

05

Why does passing cfg.mode fail when cfg is const cfg = { mode: "dark" }?

Practice

01

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;

02

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 const is 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, a const, 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.