---
title: 'Type Inference'
source: 'https://academia.sh/en/courses/typescript/type-inference'
course: TypeScript
language: en
updated: '2026-08-17T18:09:54+00:00'
license: 'CC BY-SA 4.0'
---

# Type Inference

The compiler's rules for inferring types from an initial value, context, and return expressions; widening behavior and where a type annotation is still required.

In the previous lessons' examples, the compiler already knew many types that were
never written: the types of fields after narrowing, the type of array elements, the
parameters of a `reduce` callback. Where did this knowledge come from?

The answer is **type inference**: the compiler computes an expression's type from the
information around it. Inference is what makes TypeScript usable — if every variable
needed a written type, the type layer's cost would exceed its benefit. This lesson
covers the rules of inference, its limits, and the points where a type annotation is
still required.

## Seeing the Inferred Type

The most direct way to examine inference is to ask the compiler to produce a
**declaration file**. A declaration file writes the types of the names a module
exports; inferred types show up there explicitly.

```typescript
export const unit = "C";
export let variable = "C";
export const value = 21.4;
export const measurement = { id: "s-01", value: 21.4, unit: "C" };
export const values = [21.4, 22.1];
export const mixed = [21.4, "none"];

export function average(measurements: number[]) {
  return measurements.reduce((t, d) => t + d, 0) / measurements.length;
}
```

Saved under the name `inference.ts` and compiled with
`tsc --strict --target es2022 --declaration --emitDeclarationOnly inference.ts`, an
`inference.d.ts` is written next to it:

```typescript
export declare const unit = "C";
export declare let variable: string;
export declare const value = 21.4;
export declare const measurement: {
    id: string;
    value: number;
    unit: string;
};
export declare const values: number[];
export declare const mixed: (string | number)[];
export declare function average(measurements: number[]): number;
```

This file shows six rules of inference in a single glance. They are taken in order below.

## Inference From the Initial Value, and Widening

The type of constant `unit` is not `string` but `"C"` — a set containing only that one
value. The type of constant `value` is likewise not `number` but `21.4`. The
declaration of `variable`, by contrast, has its type **widened** to `string`.

The rule is this: a declaration whose value can change cannot hold a literal type,
because the declaration can later take another value of the same type. After the
declaration `let variable = "C"`, the statement `variable = "Pa"` can be written; if
the type stayed `"C"`, this assignment would be rejected. A declaration whose value
cannot change has no need to widen.

The same reasoning holds for object fields. Although the constant `measurement` is
declared with `const`, the `unit` field's type is inferred as `string`, not `"C"`. The
reason is the distinction established in the JavaScript Fundamentals course: a `const`
declaration fixes the **binding**, not the object's content. The expression
`measurement.unit = "Pa"` is a valid JavaScript statement, so the field's type is
widened.

This behavior has a direct consequence:

```typescript
type Unit = "C" | "Pa" | "%";
type Measurement = { id: string; value: number; unit: Unit };

const raw = { id: "s-01", value: 21.4, unit: "C" };
const record: Measurement = raw;
console.log(record);
```

```text
d4.ts(5,7): error TS2322: Type '{ id: string; value: number; unit: string; }' is not assignable to type 'Measurement'.
  Types of property 'unit' are incompatible.
    Type 'string' is not assignable to type 'Unit'.
```

The assignment fails because the type of `raw.unit` has widened to `string`, and
`string` does not fit the three-valued `Unit` set. Preventing widening is the subject
of the Const Types and `as const` lesson.

For arrays, inference takes the union of the elements' types: `number[]` for `values`,
`(string | number)[]` for the mixed-element `mixed`. Because an array is mutable too,
element types are widened as well.

## Contextual Typing

Inference is not one-directional. An expression's type can also come from where the
expression is **expected**. This is called **contextual typing**:

```typescript
const values: number[] = [21.4, 22.1, 23.0];

values.forEach((v) => console.log(v.toFixed(1)));
values.forEach((v) => console.log(v.toUpperCase()));
```

```text
d1.ts(4,37): error TS2339: Property 'toUpperCase' does not exist on type 'number'.
```

No type is written on parameter `v`. The compiler takes the type from what the
`forEach` method's callback expects: since `values` is a `number[]`, the callback's
first parameter is `number`. The third line is valid; the fourth is rejected because
it calls a string method.

Contextual typing also explains why the implicit-`any` check does not trigger on
callbacks: even though the parameter looks untyped, it has a type coming from context.

## Return Type Inference and Where the Error Lands

Function return types are inferred too. In the declaration file above, the `average`
function's return type shows up as `number` even though it was never written.

Inference's convenience has a cost, and that cost shows up in **where** the error is
reported. First, the version with no return type written:

```typescript
function createMeasurement(id: string, raw: string) {
  return { id, value: raw };
}

const record = createMeasurement("s-01", "21.4");
const rounded: string = record.value.toFixed(1);
console.log(rounded);
```

```text
d2.ts(6,38): error TS2551: Property 'toFixed' does not exist on type 'string'. Did you mean 'fixed'?
```

The error is reported on the sixth line, that is, where the function is **used**. Yet
what is actually wrong is on the second line: a string was placed in the `value`
field. The same function, written together with the model's type:

```typescript
type Measurement = { id: string; value: number };

function createMeasurement(id: string, raw: string): Measurement {
  return { id, value: raw };
}

console.log(createMeasurement("s-01", "21.4"));
```

```text
d3.ts(4,16): error TS2322: Type 'string' is not assignable to type 'number'.
```

The diagnostic is now on the fourth line — at the error's source. The difference looks
trivial for a single function; in a call chain running through dozens of modules, it
determines how long it takes to find the error's source.

A practical rule follows from this: **write types at the boundaries a module exposes,
leave internal detail to inference.** The parameter and return types of a module's
exported functions, a class's public methods, and a module's data types are written by
hand. Local variables, callback parameters, and private helper functions are left to
inference.

The second reason for this rule is that a type annotation is a **contract**. A written
return type tests, when the body changes, whether the contract has been broken. An
inferred return type changes silently along with the body — and the fact that the
contract changed is only noticed once an error appears in the code that uses it.

## The Limits of Inference

Inference cannot fill every gap. There are three typical situations.

**An accumulator starting from an empty container.** For a local declaration `const
valid = []`, the compiler gathers the element type from later additions. This, again,
means the error is carried far away:

```typescript
type Measurement = { id: string; value: number };

function filter(records: Measurement[]): Measurement[] {
  const valid = [];
  for (const r of records) {
    if (r.value > 0) {
      valid.push(r.id);
    }
  }
  return valid;
}

console.log(filter([{ id: "s-01", value: 21.4 }]));
```

```text
empty2.ts(10,3): error TS2322: Type 'string[]' is not assignable to type 'Measurement[]'.
  Type 'string' is not assignable to type 'Measurement'.
```

What is wrong is the seventh line — the record's id was pushed instead of the record
itself — but the diagnostic appears on the tenth. Had it been written as `const valid:
Measurement[] = []`, the diagnostic would have shown up on the line where the push
happens.

**Data coming from outside.** The return type of a `JSON.parse` call is `any`; there
is no information for inference to use. The rule established in the previous lesson
applies here — the return is received as `unknown` and checked.

**Overly wide inference.** The type inferred from an object literal is usually looser
than intended; the `unit: string` example above is the typical case. When the intended
type is written, the compiler both narrows it and checks that the literal fits that
type.

## Summary

- Type inference computes an expression's type from its initial value, its context, and
  return expressions; inferred types can be viewed by producing a declaration file.
- Literal types widen in declarations whose value can change; a `const` declaration
  fixes the binding, so its own literal type is kept, but object fields still widen.
- Contextual typing takes an expression's type from where it is expected; callback
  parameters are typed this way.
- When the return type is not written, the error is reported where the function is
  used; when it is written, it is reported at the source.
- Types are written at exposed boundaries, internal detail is left to inference; a
  type annotation is also a contract tested when the body changes.

## Next Step

The measurement record's `unit` field could take one of three values, and that was
written by combining types that are alternatives to one another. The same operation
also has an intersection direction: values that carry the requirements of two types at
once. The next lesson covers the rules of union and intersection types, which
operations can be done on a union, and when an intersection collapses to nothing.
