# Type inference

Source: https://codewiki.com/typescript/type-inference/

> - **what**: Type inference lets TypeScript calculate static types from initializers, context, and call arguments instead of requiring annotations on every local declaration.
> - **trap**: An inferred type isn't always the narrowest literal type, and it never validates runtime data; mutable positions widen, while positions without context can produce `any`.
> - **fix**: Let obvious locals be inferred, write contracts at public APIs, empty collections, and state boundaries, and verify the result with strict checking and declaration output.

## What it is and why it exists

Type inference is the compiler process that calculates a type for an expression, variable, function result, or generic type parameter from type information it already has. It happens during type checking, and the emitted JavaScript carries none of that calculation. Inference removes repeated syntax without changing runtime values.

The most direct information comes from an initializer. `const port = 8080` already shows that the value is numeric, so adding `: number` usually imposes no extra constraint. Function parameters are different: the body must be checked before a future call supplies arguments, so parameters of ordinary named functions generally still need annotations or external context.

Another source is the expected type at the expression's location, known as contextual typing. An array's element type can provide types for the parameters of a `map()` callback, and an annotated object type can provide parameter types for methods in an object literal. Information therefore flows both out of an expression and into it from its use site.

You encounter inference in variable initialization, callbacks, object literals, function results, destructuring, and generic calls. Good inference keeps implementation code short while preserving relationships between inputs and outputs. It isn't a rule to omit every type; it identifies positions that already have enough evidence.

Public boundaries need a separate decision. If an implementation change must not alter the type callers see, write a parameter or return contract, or inspect the generated `.d.ts`. The local implementation can still rely on inference, so the two choices don't conflict.

Type inference and type narrowing are related but distinct topics. Inference establishes an initial static type; control-flow narrowing reduces an existing union after runtime checks. Complex guards and discriminated unions belong to their related topics, while this topic focuses on how inference supplies their starting point.

## How it works

The checker gathers candidate types from initializers, contextual targets, return expressions, and call arguments, then applies the rules for that position. The result must describe the current expression while allowing the later operations that position promises. That is why “narrowest” isn't a universal goal.

These common locations draw on different information. Remembering the source of the information is more reliable than memorizing the displayed type of one example.

| Location | Main information source | Common result |
| --- | --- | --- |
| `const region = "eu"` | Initializer and a non-reassignable declaration | A primitive value often retains its literal type |
| `let region = "eu"` | Initializer and a reassignable position | The string literal usually widens to `string` |
| `[1, "two"]` | Candidates from every array element | An array union that accommodates each element |
| `items.map(item => ...)` | The signature of the `map()` callback | `item` receives the array element type |
| `identity(value)` | Generic signature and call argument | A type parameter is selected for this call |
| `function total() { return 1 }` | Every reachable return expression | The function return type is inferred |

### Initialization and widening

A primitive `const` often retains its literal type because its binding can't be replaced with another value. A `let` variable normally needs to accept other values of the same kind, so string, number, and Boolean literals become their corresponding primitive types. This process is literal widening.

`const` only prevents rebinding the variable; it doesn't make object properties immutable. In `const request = { method: "GET" }`, `method` is usually `string` because `request.method = "POST"` remains possible. An ordinary array is likewise normally inferred as a mutable array, not a fixed-length readonly tuple.

When literal information must be retained, match the tool to the lifecycle. A static constant table can use `as const`; mutable state should have an appropriate union annotation; and `satisfies` can check shape while retaining expression detail. None of these tools freezes or validates input at runtime.

### Context enters from the use site

Callbacks are the most common site of contextual typing. The signature of `filter()` on `Shipment[]` already says that its predicate receives a `Shipment`, so the arrow parameter doesn't repeat that annotation. Saving the same arrow function in an unannotated variable first removes that call-site context.

Object literals can also receive context. If a variable is declared with an interface containing `onSuccess(message: string): void`, then `message` in `onSuccess(message) { ... }` is inferred as `string`. If you create an untyped object first and assign it to the interface later, the missing context isn't retroactively applied when the object was created.

Context isn't a type assertion. The expression must still be assignable to its target, so a misspelled property or incompatible result produces a diagnostic. An assertion can instead tell the checker to disregard a real incompatibility, which gives it the opposite evidentiary role.

### Multiple candidates and common types

Array elements and multiple return branches supply more than one candidate. The checker looks for a result that can accommodate them, forming a union when needed. For example, `[1, "two"]` usually becomes `(string | number)[]` instead of arbitrarily choosing one element's type.

An array of class instances doesn't necessarily rise to a shared base class automatically. If the candidates are `Rhino`, `Elephant`, and `Snake`, the result can retain their union instead of guessing that the author wants `Animal[]`. Write `Animal[]` at the declaration when that base-class contract is intentional.

An empty collection supplies no element candidates, so context matters especially strongly. At a context-free boundary in a strict project, an empty array can trigger an implicit `any[]` diagnostic; in another expression position it may receive a contextual element type. Don't rely on one isolated example as a universal answer—give empty collections an element type.

### Generic call inference preserves relationships

A generic function puts the same type parameter in several positions, and the checker infers its replacement for each call from the arguments. `pluck<T, K extends keyof T>` infers `T` from the record array and `K` from the key argument, so its result is the corresponding `T[K][]`. `any` can't express that relationship.

A constraint only restricts candidates and exposes members the function body may use. `K extends keyof T` doesn't check at runtime that a string is an object key, and the generic syntax is erased from the output. Values arriving from JSON or JavaScript still require runtime validation.

When several arguments provide conflicting candidates for one type parameter, the result might be a union, a wider type, or an error depending on the signature's input and output positions. Don't hide the result with a forced assertion; first ask whether one type parameter has been made responsible for unrelated relationships.

### Results and contextual targets

Without a return annotation, the checker combines every reachable `return` expression. If one branch returns a string and another returns no value, the result usually includes `undefined`. A caller's expectation doesn't erase a return branch that really exists in the implementation.

When a function expression is assigned to an existing function type, its return expressions are also contextually checked against that target. This keeps an error near the implementation, but the target still doesn't convert runtime values. A number result doesn't automatically become a string just because the context asks for one.

Inference for an `async` function reflects the Promise wrapper. If the body returns an `Invoice`, callers see `Promise`; multiple branches must still produce a compatible result before wrapping. A public asynchronous boundary often deserves an explicit `Promise<...>` to keep implementation mistakes out of the contract.

A directly or indirectly recursive function may not provide enough ordering information for a stable inferred return, and it is more likely to produce circular-reference diagnostics. Annotating the recursive entry both breaks the inference cycle and records the common result every recursive branch must satisfy.

### Destructuring and defaults

A destructured variable gets its type from the value being destructured. `const { id, total } = order` preserves each property type, while array destructuring chooses positional types from array or tuple information. Only a tuple associates fixed indexes with specific element types; an ordinary array makes no such positional promise.

Parameter destructuring still needs a source. `function print({ id }) {}` gives the object parameter no type, and strict mode won't invent a complete object contract by reading how the body uses it. Annotate the whole parameter object instead of asserting the extracted local name.

Default values participate in availability analysis. After a default is applied to an optional parameter, the function body can normally use a type with `undefined` removed, while the call signature still lets callers omit that argument. The body type and caller contract describe two observation points.

Object rest properties and array rest elements also derive new types, but dynamic keys and index signatures can reduce precision. If complex destructuring makes diagnostics hard to locate, name and annotate the input object first, then destructure it shallowly instead of adding assertions.

## Examples

These four examples progress from initialization and widening to callback context, generic call inference, and a public return boundary. Each file was checked under `--strict` with TypeScript 6.0.3 and then executed locally with `npx tsx`.

### Observe initialization and widening

Type-level assertions verify three inferred results. The primitive `const` retains a literal, while the `let` and mutable object property allow later values of the same kind.

<!-- quick -->

```typescript
// file: inference-basics.ts
type Equal<A, B> =
  (<T>() => T extends A ? 1 : 2) extends
  (<T>() => T extends B ? 1 : 2) ? true : false;
type Expect<T extends true> = T;

const homeRegion = "eu-west";
let activeRegion = "eu-west";
const service = { state: "ready", attempts: 0 };

type HomeIsLiteral = Expect<Equal<typeof homeRegion, "eu-west">>;
type ActiveIsString = Expect<Equal<typeof activeRegion, string>>;
type StateIsString = Expect<Equal<typeof service.state, string>>;

activeRegion = "us-east";
service.state = "busy";
service.attempts += 1;

console.log(homeRegion);
console.log(activeRegion);
console.log(`${service.state}: ${service.attempts}`);
```

```text
eu-west
us-east
busy: 1
```

<!-- /quick -->

`Expect` emits no JavaScript, but it makes type checking fail if the inference differs from the expectation. No assertion forces a result into the target type here, so the test observes the type the checker actually computed.

The `service` variable itself can't be rebound, but its properties remain mutable. To restrict `state` to a few legal states, annotate the property with a union or define a state model at a higher level rather than assuming `const` already established that contract.

### Use callback context

The element annotation on `shipments` supplies context to the parameters of both the `filter()` and `map()` callbacks. The second `map()` parameter also becomes `number` through the standard array signature.

```typescript
// file: contextual-callbacks.ts
type Shipment = {
  id: string;
  kilograms: number;
};

const shipments: Shipment[] = [
  { id: "PK-104", kilograms: 7 },
  { id: "PK-105", kilograms: 12 },
  { id: "PK-106", kilograms: 18 },
];

const heavyLabels = shipments
  .filter((shipment) => shipment.kilograms >= 10)
  .map((shipment, index) =>
    `${index + 1}. ${shipment.id} (${shipment.kilograms} kg)`,
  );

console.log(heavyLabels.join("\n"));
```

```text
1. PK-105 (12 kg)
2. PK-106 (18 kg)
```

The callbacks return template strings, so `heavyLabels` is inferred as `string[]`. One domain contract on the array gives the pipeline enough information without repeating `Shipment`, `number`, and `string`.

If the function in `.filter((shipment) => ...)` were first written independently as `const isHeavy = (shipment) => ...`, that declaration would have no array-method context. Under `noImplicitAny`, `shipment` produces an implicit `any` diagnostic; the fix belongs on the standalone function parameter, not in an assertion at the call.

### Preserve property relationships from generic arguments

The two arguments to `pluck()` jointly determine its result element type. Selecting `"total"` yields `number[]`, while selecting `"paid"` yields `boolean[]`, with one implementation.

```typescript
// file: generic-pluck.ts
function pluck<T, K extends keyof T>(
  rows: readonly T[],
  key: K,
): T[K][] {
  return rows.map((row) => row[key]);
}

const orders = [
  { id: "A-17", total: 42, paid: true },
  { id: "B-04", total: 19, paid: false },
];

const totals = pluck(orders, "total");
const paidFlags = pluck(orders, "paid");

const grandTotal = totals.reduce((sum, total) => sum + total, 0);
console.log(`total: ${grandTotal}`);
console.log(`paid: ${paidFlags.join(", ")}`);
```

```text
total: 61
paid: true, false
```

`K extends keyof T` rejects an invalid key at the call and makes `row[key]` legal in the implementation. A broad `unknown[]` result would discard this property relationship, while `any[]` would additionally spread unchecked operations to callers.

Generics only preserve a static relationship. If `key` comes from a command line or network, first check that it belongs to the current object's allowed key set, then enter this typed function.

### Fix a public return contract

Locals and the `reduce()` callback inside the function still rely on inference, while the public result is explicitly `InvoiceSummary`. If a refactor omits a field or returns an incompatible type, the diagnostic appears inside the function.

```typescript
// file: public-boundary.ts
type InvoiceSummary = {
  count: number;
  totalCents: number;
};

export function summarizeInvoices(
  amounts: readonly number[],
): InvoiceSummary {
  const totalCents = amounts.reduce(
    (sum, amount) => sum + amount,
    0,
  );

  return { count: amounts.length, totalCents };
}

const summary = summarizeInvoices([1299, 2500, 499]);
console.log(`${summary.count} invoices`);
console.log(`${summary.totalCents} cents`);
```

```text
3 invoices
4298 cents
```

An explicit boundary doesn't require annotations on every local. `totalCents`, `sum`, `amount`, and `summary` all have enough information, and repeating their types would only add maintenance sites.

If the function is used only inside one file and its callers may naturally change with its implementation, return inference can also be appropriate. Whether to annotate depends on whether this location is a stable contract, not on the function's line count.

## Pitfalls

> **Pitfall:** Treating inference as if it always chooses the most precise type. Mutable bindings, object properties, and array elements normally leave room for future writes, so literals can widen.

**Fix:** First decide whether the value is actually immutable. Use `as const` for constant data, an explicit union for mutable state, and `satisfies` for shape checking; don't add assertions to every expression merely to chase a narrower type.

> **Pitfall:** Assuming an empty array is always `any[]` or always `never[]`. Its result depends on context, control flow, compiler options, and declaration position, so an isolated example can't answer for every site.

**Fix:** Put the element contract where the empty collection is created, such as `const jobs: Job[] = []`. This avoids implicit `any` and makes the first incompatible write fail at the source.

> **Pitfall:** Saving a callback in an unannotated variable, then expecting its later use by an array method to fill in its parameter type. Contextual typing happens where an expression is checked; it doesn't flow backward and rewrite an independent declaration already checked.

**Fix:** Keep short callbacks inline, or annotate the parameters and result of a reusable callback. Don't silence a `noImplicitAny` diagnostic with `any`, because that disables useful checks in the callback body.

> **Pitfall:** Deleting a public function's return annotation because the current implementation is “obviously inferred.” A later condition, error sentinel, or optional field can silently widen the exported type and push the break onto callers.

**Fix:** Annotate stable public API results and inspect declaration emit when publishing a library. The implementation can still make extensive use of local inference.

> **Pitfall:** Taking a concrete-looking type after `JSON.parse(raw)` as successful inference. Its standard declaration returns `any`, so frictionless property access means checking has been bypassed, not that the data was proved.

**Fix:** Immediately place untrusted results in `unknown`, validate the object, fields, and domain rules, then construct the domain value. Inference can propagate existing static evidence; it can't manufacture runtime evidence.

<!-- deep -->

## Inferred types are static facts

An inferred type is the checker's static description of an expression, not a label attached to the value at runtime. After emit, `const count = 3` is still just a JavaScript number, and runtime code can't ask whether it was inferred as `3` or `number`. Type erasure also means inference itself can't validate input.

Static types depend on the declarations visible during checking. An incorrect third-party `.d.ts` can produce a very precise inference that is completely wrong at runtime. Precision isn't truth; the evidence chain also includes declaration quality and external-boundary validation.

`any` contaminates that evidence chain. Property access, calls, and assignments on `any` generally continue to produce `any`, so error-free code may mean the checker was told to stop proving facts rather than that inference was strong. When diagnosing inference, find the earliest upstream source of `any` first.

`unknown` behaves in the opposite way. It accepts any value but blocks type-specific operations until a control-flow check or validator supplies evidence. Using `unknown` for external input lets each later concrete type represent an auditable upgrade.

## Empty collections and control-flow evolution

An empty array literal supplies no element candidates. A target such as `Job[]` lets the checker use that element type; without context, strict options can report implicit `any[]`. In some local control flows, the checker can also evolve the observed array type from later writes.

That evolution is an analysis rule, not a runtime array changing its type. Different read sites can have different control-flow information, and an export boundary can't assume that every caller has observed the same writes. A domain collection therefore shouldn't use its first few `push()` calls as its public contract.

An empty object has a related modeling problem. The inferred type of `const settings = {}` doesn't acquire properties from nowhere later, so adding fields incrementally usually errors. When construction truly has stages, use an explicit builder, a local optional-state model, or one complete object instead of a broad assertion.

An initial `null` likewise can't express a full lifecycle. Under strict null checking, a variable that later stores a `Connection` should say `Connection | null`. An annotation doesn't fight inference here; it supplies the future legal state that the initializer can't express.

## Ownership of generic candidates

One type parameter should express one relationship you can name. In `pluck()`, `T` owns the record type and `K` owns the record key. If one `T` simultaneously controls an input, fallback, callback result, and cache entry, a change in any one position can alter inference everywhere else.

Input positions commonly provide candidates for a type parameter, while a contextual target can sometimes contribute too. A constraint rejects candidates that lack required capabilities, but it doesn't automatically replace the result with the constraint. `T extends { id: string }` should still preserve other fields from the argument when possible.

Explicit type arguments are appropriate when the checker can't choose uniquely but the caller genuinely owns the choice. They shouldn't be routine error-suppression buttons. If `load(raw)` merely asserts the result of `JSON.parse` to `T`, the caller's type argument supplies no runtime evidence.

When inference fails, first name intermediate results and inspect the actual types shown by the editor or declaration output. Then check whether there are too many type parameters, whether constraints sit in the wrong place, whether an argument widened too early, and whether the result context is truly part of the contract. Consider explicit type arguments last.

## Record inference with type queries

In a type position, `typeof` obtains the static type of a value. It doesn't execute JavaScript's runtime `typeof` check or return a string. Combined with indexed access and `keyof`, it lets later types derive from one source-owned data value.

This derivation prevents separately handwritten structures from drifting, but it also carries forward any widening at the source. If a configuration property's type is already inferred as `string`, a later `typeof config.mode` can't recover its original literal. Required precision must be retained at the source declaration.

Type-level tests can turn an inferred result into a compilation gate. Common approaches define check-only helpers such as `Equal` and `Expect`, or include assignments that should succeed and rejected assignments marked with `@ts-expect-error`. Those tests expose inference-rule changes when the compiler is upgraded.

Don't use a large inferred type verbatim as documentation. Name important domain concepts and show only what callers need in public signatures; `typeof` is useful for keeping facts synchronized, but it shouldn't expose every accidental detail of an internal object as API.

## Compiler options are part of inference

The same source can produce different diagnostics or usable types under different compiler options. `noImplicitAny` controls whether missing information may silently become `any`, while `strictNullChecks` determines whether `null` and `undefined` remain distinct members. Any claim about an inferred result needs an effective configuration.

`strict: true` is a strong baseline, but it doesn't include every more conservative option. `noUncheckedIndexedAccess` adds `undefined` to an indexed read whose presence wasn't proved, changing the type visible to later expressions. This isn't an aside to inference; it is part of the contract environment supplied to the checker.

Command-line flags, inherited `tsconfig` files, and the project selected by an editor can all change that effective configuration. When an editor and CI “infer differently,” run `tsc --showConfig`, confirm which project owns the file, and then compare compiler versions. Source alone can't explain a configuration mismatch.

Library publishers must also test declarations with the oldest compiler in their supported range. A newer compiler can emit type syntax that an older version can't parse or infer equivalently. Version compatibility is part of the publication contract, not merely whatever works in the author's editor.

## Declaration emit exposes inference

With `declaration` or `emitDeclarationOnly`, the compiler writes the visible types of exported declarations into `.d.ts`. An exported function without an explicit return annotation turns its inferred result into the publication contract, so implementation details can enter source control, package artifacts, and caller diagnostics.

That doesn't mean every export needs a handwritten complete type. A small constant table may intentionally publish exact literals, and a generic factory may rely on inference to preserve relationships. The key question is whether the team deliberately accepts the particular shape in the generated declaration.

Library review should treat declaration output as a build artifact. Run the same TypeScript 6 declaration command before and after a change, compare exported signatures, and keep type tests for calls that should be accepted and rejected. JavaScript runtime tests alone can't see this kind of contract drift.

Application code can borrow the same method to diagnose complicated inference. Temporarily export an intermediate declaration and generate a `.d.ts`; this is often more direct than guessing from an error message. Remove the diagnostic export afterward so it doesn't accidentally become real API.

## Where annotations belong

Parameters generally need annotations because a function body must be checked without knowing future call arguments. Callback parameters can omit them when reliable context exists; once a callback becomes an independent declaration, it owns a boundary of its own. Different positions require different evidence.

Return annotations fit stable interfaces, recursive functions, and functions whose drift should fail in the implementation. Local helpers can rely on inference, especially when their result shape is just a natural product of nearby code. A blanket rule that every function must—or must not—declare a return type loses this boundary distinction.

Variable annotations fit empty collections, state whose full lifecycle is wider than its initializer, and objects intentionally widened to an interface. Repeating a primitive type adds little when the initializer already expresses the intent. An annotation should add a constraint or stability, not a feeling of statistical coverage.

Finally, inspect where errors land. A good boundary makes an incompatible implementation fail at the producer, a bad argument fail at the call, and invalid external data fail at parsing. If the error can surface only in a distant consumer, the inference chain probably lacks a contract that should be explicit.

<!-- /deep -->

[Checkpoint: typescript/type-inference](https://codewiki.com/typescript/type-inference/#checkpoint)

## Further reading

- [TypeScript Handbook: Type Inference](https://www.typescriptlang.org/docs/handbook/type-inference.html)
- [TypeScript Handbook: Everyday Types and type annotations](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#type-annotations-on-variables)
- [TypeScript Handbook: Functions and generic calls](https://www.typescriptlang.org/docs/handbook/2/functions.html)
- [TypeScript 3.4 release notes: `const` assertions](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions)
- [TypeScript 4.9 release notes: the `satisfies` operator](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html#the-satisfies-operator)
- [TypeScript Handbook: Creating `.d.ts` files from JavaScript](https://www.typescriptlang.org/docs/handbook/declaration-files/dts-from-js.html)
