Skip to content
Theory · Functions · lesson 7 of 10

Callbacks and contextual typing

In the second theme a parameter without a type was error TS7006: inference had nothing to read. Yet JavaScript callbacks are written without any types, and in TypeScript such code compiles too:

TypeScript
const prices = [100, 250];
const total = prices.map((p) => p + 300);
console.log(total.join(" ")); // 400 550

p has no annotation, and there is no error either. Hover over p: the hover shows (parameter) p: number. How did TypeScript know?

Remember the machine's plate. map has a plate too — its type. It says what map will do with your function: call it for each element and pass it the element, its index and the array itself. When the arrow stands right inside the call prices.map(...), TypeScript reads that plate and labels the parameters itself. p is an element of an array of numbers, so it is a number.

The type from the call site

Contextual typing is the name for inferring a type from the place where a function stands. Ordinary inference goes from the value to the name: let count = 5 gives number. Contextual typing goes the other way — from the place to the function:

  • an arrow passed to map on a number[] array gets a number parameter;
  • an arrow put in a variable with a function type takes its parameter types from that type;
  • an arrow passed to a function with a function parameter gets its types from that function's signature.

That is why callbacks written right inside a call usually need no annotations. The result type is inferred as always — from the body: p + 300 gives number, and total gets the type number[].

Press "Next step" and watch where each arrow gets its parameter's type — and where it has nowhere to get it from.

LiveWhere a callback gets its typesstep 1 of 8

TypeScript inferred an array of numbers from the literal.

Watch where each arrow gets its parameter's type

Fewer parameters are fine

map passes the callback three arguments, while the arrow (p) => p + 300 takes one. A function with fewer parameters fits a function type with more: it does not read the extra arguments, and JavaScript allows that. Almost every callback works this way: the index and the array itself are rarely needed.

The other way round is not allowed. A function that needs more parameters than the calling code will pass does not fit:

TypeScript
type Visit = (x: number, i: number) => void;

const both: Visit = (x: number, i: number, extra: number) => { // ✗
    console.log(x, i, extra);
};
TEXT
error TS2322: Type '(x: number, i: number, extra: number) => void' is not assignable to type 'Visit'.
  Target signature provides too few arguments. Expected 3 or more, but got 2.

The second line of the message gives the reason: "the type being assigned to provides too few arguments — 3 or more are needed, and there are 2". Whoever calls a Visit passes only x and i, so extra would always be undefined.

Interviewers like to ask why a callback with fewer parameters fits a type that has more. The answer: the calling code passes the extra arguments, and the function is allowed not to read them. The reverse would be dangerous: the function would wait for an argument that nobody passes.

Common mistake

A callback was moved out of the call into a constant, to give it a name:

TypeScript
const prices = [100, 250];
const addFee = (p) => p + 300; // ✗
console.log(prices.map(addFee).join(" "));
TEXT
error TS7006: Parameter 'p' implicitly has an 'any' type.

This is where the plate analogy ends. A person can read the plate in advance and remember it, but TypeScript reads map's plate only when the arrow stands right inside the map call. The fact that addFee is passed to map one line below tells nothing about the type of p. When you move a callback out, write the parameter's type yourself, or give the constant a function type, like const log: Visit above:

TypeScript
const prices = [100, 250];
const addFee = (p: number) => p + 300;
console.log(prices.map(addFee).join(" ")); // 400 550

Key points

  • Contextual typing takes parameter types from the place where a function stands: the map call, the variable's type, the signature of the function it is passed to.
  • An arrow right inside a call needs no annotations, while an arrow without types moved into a constant gives error TS7006.
  • A function with fewer parameters fits a function type with more; one with more does not.
  • A callback moved out of the call gets its parameter types explicitly: (p: number) => p + 300.