skip to content

Type Argument Inference

Why you almost never write the type arguments yourself, how the compiler picks them from the values you pass, and what to do when it picks something too wide or too narrow. This is the part of generics that separates 'I can read types' from 'I can design an API that infers well'.

part ofTypeScriptoverview, primer and where to startread it →
on this pageshow

questions

10

In TypeScript, given `declare function first<T>(items: T[]): T | undefined`, what is the type of `first([1, 2, 3])`, and where did the compiler get `T` from?

level: juniorimportance: must knowfreq 72%

answer

  1. the compiler reads the arguments
  2. parameter positions are inference sites
  3. number[] matched against T[]
  4. return type consumes T, never supplies it
  5. an empty array literal offers nothing

basics

~20 s

first([1, 2, 3]) has type number | undefined. TypeScript matches the argument type number[] against the parameter type T[] at the call site, infers T as number, and substitutes that into the declared return type.

solid answer

~50 s

`first([1, 2, 3])` is `number | undefined`. Every parameter position where a type parameter appears is an *inference site*: the compiler works out each argument's own type, matches it structurally against the declared parameter type, and whatever lines up opposite `T` becomes a candidate. Here the array literal is `number[]`, matching it against `T[]` puts `number` opposite `T`, so `T` is fixed to `number` and the return type `T | undefined` becomes `number | undefined`. That is why you almost never write `first<number>([1, 2, 3])` — the explicit type argument is only needed when nothing at the call pins `T` down. Two results worth knowing: `first([])` is just `undefined`, because an empty array literal is `never[]`, and `first([1, "x"])` is `string | number | undefined`, because the literal's element type is already a union before inference runs.

code

typescript · 7 lines
typescript
declare function first<T>(items: T[]): T | undefined;

export const a = first([1, 2, 3]);      // number | undefined
export const b = first(["x"]);          // string | undefined
export const c = first([1, "x"]);       // string | number | undefined
export const d = first([]);             // undefined — T infers as never
export const e = first<number>([1, 2]); // explicit type argument, same result

go deeper

for a junior

Be ready to read a generic signature aloud and say which argument gave the compiler its type. Knowing that first([1, 2, 3]) is number | undefined and that you did not have to write <number> is the expected answer.

for a middle

Explain the mechanics: argument types are computed first, matched structurally against the declared parameter types, and each match contributes a candidate. Be able to say why the return position contributes nothing.

for a senior

Show you can debug from this. When a colleague reports a surprising inferred type, trace it back to the argument that supplied the candidate — the empty-array-to-never case is the classic one to spot in review.

for a principal

Own the API-shape consequence: a signature whose type parameters have no argument-side inference site pushes work onto every caller. Judge new generic helpers by whether a normal call infers correctly with zero annotations.

## The two halves of a generic call A generic function declares one or more **type parameters** — placeholders in angle brackets that stand for a type the caller decides. `declare function first<T>(items: T[]): T | undefined` declares one, `T`, and then uses it twice: in the parameter type `T[]` and in the return type `T | undefined`. The declaration never says what `T` is. The caller decides, and in ordinary code the caller decides *implicitly*, just by passing an argument. Working out `T` from the arguments is called **type argument inference**. ## Inference sites An **inference site** is a place in the parameter list where a type parameter appears. When you write a call, the compiler: 1. Works out the type of each argument on its own, before looking at the signature. 2. Matches that type structurally against the declared parameter type. 3. Wherever a type parameter appears on the parameter side, whatever sits opposite it on the argument side becomes a **candidate** for that parameter. 4. Fixes the type parameter to a candidate, then substitutes it everywhere in the signature — including the return type. Walking `first([1, 2, 3])` through those steps: the array literal's own type is `number[]`; matching `number[]` against `T[]` lines `number` up opposite `T`; `number` is the only candidate, so `T` is `number`; the call's type is therefore `number | undefined`. ```ts declare function first<T>(items: T[]): T | undefined; const value = first([1, 2, 3]); // ^? number | undefined ``` Notice that the return type did no work. It consumed `T`; it did not supply it. A type parameter that appears **only** in the return type has no inference site at all, and the compiler has to fall back to something else. ## The argument's own type is decided first Inference never reaches inside the argument to redecide it. Whatever the argument's type already is, that is what gets matched: ```ts first(["x"]); // string | undefined first([1, "x"]); // string | number | undefined — the literal is (string | number)[] first([]); // undefined — an empty array literal is never[], so T is never ``` The empty-array case is the one that surprises people. `first([])` is not an error and does not produce `any`; the element type of `[]` is `never`, so `T` is `never` and `T | undefined` collapses to `undefined`. In real code this shows up when an accumulator starts empty. Annotate the variable — `const rows: Row[] = []` — and the call infers `Row` as intended. ## The call's context can also supply a candidate Arguments are the main source, but not the only one. If a call's result flows into a position with a known expected type, that expected type can supply a candidate too — with lower priority than arguments: ```ts declare function make<T>(): T; const name: string = make(); // T is string, taken from the annotation ``` Because argument candidates outrank the contextual one, an annotation cannot override what the arguments already decided. `const w: string | number = pick(1)` still infers `number` for a `pick<T>(v: T): T`. ## Writing the type argument yourself Explicit type arguments are always available as an escape hatch: `first<number>([1, 2, 3])`. Supplying them is all-or-nothing — you must give one for every type parameter unless the trailing ones have defaults, so a two-parameter generic called with a single type argument is rejected. In practice you reach for explicit type arguments only when the call has no usable inference site, or when inference lands somewhere you did not want. ## None of this exists at runtime Types are erased. `first<T>` compiles to an ordinary JavaScript function with no `T` anywhere; inference is a compile-time bookkeeping exercise that produces better editor types and better errors, and costs nothing when the program runs. A function cannot branch on `T`, and no `T` is available to inspect. ## Common misreadings Inference is not a runtime check, it is not driven by the variable name you assign to, and it does not fall back to `any` when it is unsure. It is a structural match between the argument types you actually passed and the parameter types the signature declares — and once you can narrate that match, most "why is this type weird?" questions answer themselves.

  • What does `T` become when the argument is an empty array literal, and why does that bite in real code?
    An empty array literal has type `never[]`, so `T` infers as `never` — `first([])` is just `undefined`. It bites when an accumulator starts out empty and everything downstream ends up typed `never`. The fix is to give the variable a type where it is declared, `const rows: Row[] = []`, or to pass an explicit type argument at the call.
  • Does the type parameter's appearance in the return type help the compiler work out `T`?
    No. The return position consumes `T`; it never supplies a candidate. A type parameter that appears only in the return type has no inference site among the arguments, so it falls back to its constraint or to `unknown` unless the caller writes the type argument. What can help is the call's contextual type — an annotated variable receiving the result — but that has lower priority than any argument candidate.
  • Is there any runtime cost to a generic call?
    None. Type parameters are erased during compilation, so the emitted JavaScript is the same function with the same arguments — there is no reified `T`, no check, and no metadata. That also means a generic function cannot inspect or branch on `T` at runtime; if behaviour must depend on the type, you have to pass a real value, such as a discriminant field or a parser.

saying these in an interview costs you the question

  • Claims T is looked up at runtime from the value
  • Says inference falls back to any when unsure
  • Thinks the return type position drives inference
  • Believes you must always write the type argument
  • Assumes first([]) is an error or gives any

context

open as a page

In TypeScript, `[1, 2, 3].map(x => x * 2)` compiles with `x` typed as `number`, but extracting the same arrow into `const cb = (x) => x * 2` and calling `.map(cb)` fails to compile. Why?

level: middleimportance: must knowfreq 70%

basics

~20 s

A function expression written directly in an argument position is contextually typed: its parameter types come from the expected callback type. A standalone const has no such context, so its parameter is an implicit any error under strict.

open as a page

What does the `const` modifier on a type parameter do — `declare function f<const T>(x: T): T` in TypeScript 5.0 and later — and when does it have no effect?

level: middleimportance: should knowfreq 40%

basics

~20 s

It makes inference treat the argument as if the caller had written a const assertion: object literals come back with readonly literal-typed properties, arrays as readonly tuples. It affects only expressions written at the call site, and nothing at runtime.

open as a page

In TypeScript, `declare function box<T>(x: T): { value: T }` called as `box("red")` produces `{ value: string }` rather than `{ value: "red" }`. Why does the literal widen, and how would you change the signature to keep it?

level: middleimportance: should knowfreq 50%

basics

~20 s

TypeScript infers the fresh literal type "red" and then widens it to string, because nothing in the signature asks for a literal. Constraining the type parameter with T extends string, or marking it const, preserves the literal.

open as a page

For `declare function pair<T>(x: T, y: T): T[]` in TypeScript, how does the compiler resolve `T` when the two arguments suggest different types — compare `pair(1, "x")` with `pair(animal, dog)` where `Dog extends Animal`?

level: middleimportance: should knowfreq 48%

basics

~20 s

TypeScript collects one candidate per inference site and keeps the candidate that is a supertype of the others, so pair(animal, dog) gives Animal[]. When no candidate covers the rest, pair(1, "x") is a compile error rather than a silent union.

open as a page

In TypeScript, for `declare function make<T>(): T`, what type does `T` get at `const x = make()`, and what changes if the signature is `declare function makeStr<T extends string>(): T`?

level: middleimportance: should knowfreq 42%

basics

~10 s

With no argument mentioning T, inference has nothing to work from: T falls back to its constraint when it has one. So make() gives unknown, while makeStr() with T extends string gives string.

open as a page

What problem does TypeScript's built-in `NoInfer<T>` utility type solve, and how would you use it in a generic function signature?

level: seniorimportance: should knowfreq 34%

basics

~20 s

NoInfer marks a parameter position as check-only: it stops that position from contributing an inference candidate, so the type parameter is fixed by the other arguments and a stray default or fallback argument is reported as an error instead of widening the type.

open as a page

In TypeScript, `declare function f<T extends readonly unknown[]>(arr: T): T` called as `f([1, "a"])` infers `(string | number)[]` rather than the tuple `[1, "a"]`. What signature changes make it infer a tuple, and what does each produce?

level: seniorimportance: should knowfreq 28%

basics

~20 s

An array literal only becomes a tuple when something asks for one. Make the parameter a rest parameter and TypeScript infers positionally, or add a const type parameter with a readonly array constraint to infer a readonly tuple of literal element types.

open as a page

Users of your helper `declare function pipe<A, B>(f: (a: A) => B): (a: A) => B` report that in `pipe(x => x)` the parameter `x` is `unknown`. Why does that happen, and what are your options for fixing it?

level: seniorimportance: should knowfreq 40%

basics

~20 s

A callback parameter consumes A instead of supplying it, so nothing at the call gives A a candidate and it falls back to unknown. Annotate the parameter, add a constraint, or take the value as an argument.

open as a page

You maintain a widely used TypeScript library whose helpers must see the caller's literal values precisely. How do you decide between requiring a const assertion at each call site, adding `const` type parameters, and using `NoInfer` — and what does each choice cost?

level: principalimportance: nice to knowfreq 20%

basics

~20 s

Decide by who bears the burden and what it costs them. Caller-side assertions keep signatures simple but fail silently when forgotten; const type parameters guarantee precision at the price of deeply readonly types everywhere; NoInfer only fixes arguments that should conform, not infer.

open as a page