skip to content

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%

answer

  1. type information can flow inwards
  2. the expected type at that position
  3. a standalone const has no expectation
  4. implicit any under noImplicitAny
  5. annotate the parameter or the variable

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.

solid answer

~50 s

Type information flows *into* the inline arrow. When a function expression sits in an argument position, the compiler already knows the expected type of that parameter — for `map` it is a callback whose first parameter is the array's element type — and it pushes that type onto the arrow's parameters. That is contextual typing, and it is why `x` is `number` without you writing anything. Pull the arrow out into `const cb = (x) => x * 2` and there is no expected type at the declaration, nothing supplies `x`, and under `noImplicitAny` you get "Parameter 'x' implicitly has an 'any' type". The fixes are all about restoring a context: annotate the parameter, `const cb = (x: number) => x * 2`; or annotate the variable with a function type, `const cb: (x: number) => number = ...`, which pushes the type down into the arrow again; or leave the callback inline.

code

typescript · 11 lines
typescript
const nums = [1, 2, 3];

// Contextual typing: x gets its type from the signature of Array.prototype.map
export const doubled = nums.map(x => x * 2);

// Written standalone with no contextual type, x would be an implicit any error,
// so give it a type — on the parameter, or on the variable.
const cbAnnotated = (x: number) => x * 2;
const cbTyped: (x: number) => number = x => x * 2;

export const same = nums.map(cbAnnotated).concat(nums.map(cbTyped));

go deeper

for a junior

Recognise the error message "Parameter 'x' implicitly has an 'any' type" and know the two everyday cures: annotate the parameter, or keep the callback inline where the array method supplies the type.

for a middle

Explain contextual typing as information flowing inward from an expected type, and show both cures work for the same reason — an annotated variable is an expected type just as an argument position is.

for a senior

Use this when reviewing a codebase's implicit-any noise: decide whether a helper should be inlined, given a named function type, or reshaped so callers never have to annotate callback parameters at all.

for a principal

Treat inferability as an API quality attribute. A library whose callbacks only type correctly when consumers annotate them will accumulate any in every downstream codebase, so shape signatures so context reaches every callback parameter.

## Two directions of type information Most of the time you picture types flowing outward: a value has a type, and it propagates into whatever you build from it. **Contextual typing** is the opposite direction. When the compiler already knows what type is *expected* at some position, it pushes that expectation inward and uses it to type an expression that would otherwise be ambiguous. Function expressions are the main beneficiary, because an un-annotated parameter has no type of its own to offer. ## Where the context comes from The declared signature supplies it. `Array.prototype.map` is declared roughly as a generic method taking a callback whose first parameter is the array's element type, whose second is a `number` index, and whose third is the array itself. When you write `[1, 2, 3].map(x => x * 2)`, the compiler: 1. Knows the receiver is `number[]`, so the callback's expected type is `(value: number, index: number, array: number[]) => U`. 2. Sees a function expression in that argument position. 3. Assigns the expected parameter types positionally to the arrow's parameters, so `x` is `number`. 4. Infers the callback's return type `number` from the body, which fixes `U` and makes the whole call `number[]`. Step 3 is contextual typing; step 4 is ordinary inference. They cooperate on every callback you write. ```ts const nums = [1, 2, 3]; const doubled = nums.map(x => x * 2); // x: number, doubled: number[] const labels = nums.map((x, i) => `${i}:${x}`); // x: number, i: number ``` ## Why the extracted arrow loses it `const cb = (x) => x * 2;` is a declaration with no annotation on the variable and no annotation on the parameter. There is no expected type anywhere in sight — the compiler is being asked to type the function *before* it knows where it will be used, and it cannot look ahead at the `.map(cb)` further down the file. So `x` gets the implicit `any` type, which `noImplicitAny` (on under `strict`) reports as error TS7006. The error is at the declaration, not at the call site, which is what confuses people: the call looks identical to the version that worked. Note what the error is *not*. It is not that `cb` is incompatible with `map`; once `x` is annotated, `nums.map(cb)` is accepted happily. The failure is purely that the standalone declaration has no context to draw from. ## Three ways to restore a type ```ts // 1. Annotate the parameter. const cb1 = (x: number) => x * 2; // 2. Annotate the variable with a function type — the context flows back in, // so the arrow's own parameter can stay bare. const cb2: (x: number) => number = x => x * 2; // 3. Keep it inline, where the argument position is the context. const doubled = [1, 2, 3].map(x => x * 2); ``` Option 2 is the same mechanism as the inline case: an annotated variable is an expected type, so the initializer is contextually typed by it. That is worth demonstrating in an interview, because it shows you understand *contextual typing* rather than having memorised "annotate your parameters". ## What else the context buys you Contextual typing is not limited to callbacks in argument positions. It reaches methods and function-valued properties of an object literal that is assigned to an annotated variable or passed to a typed parameter: ```ts type Handler = { onEvent(e: { id: string }): void }; const h: Handler = { onEvent(e) { console.log(e.id); } }; // e is typed ``` It also explains why you may write fewer parameters than the callback declares. `map` passes three arguments, yet `x => x * 2` is accepted — a function that ignores trailing parameters is assignable to one that declares them, so you only spell out what you use. ## Limits worth naming The context must be knowable at that position. A bare `const`, an object literal with no target type, and a function stored in an untyped structure all lose it. And contextual typing supplies parameter types; it does not validate the body against some expectation beyond the return type. Finally, when the expected callback type mentions a type parameter that nothing else at the call pins down, the parameter still gets a type — just an uninformative one — which is a separate diagnosis from the implicit-any case here.

  • Why is a one-parameter callback accepted where the signature declares three parameters?
    Function assignability ignores extra parameters on the target side: a function that takes fewer arguments can stand in for one that is called with more, because the surplus arguments are simply ignored at runtime. That is why `map(x => x * 2)` is fine even though the callback is invoked with value, index, and array. The reverse is rejected — you cannot declare *more* parameters than the expected type provides.
  • Does contextual typing apply anywhere other than function arguments?
    Yes. Any position with a known expected type provides context: an annotated variable's initializer, the methods and function-valued properties of an object literal assigned to an annotated variable or passed to a typed parameter, an array literal element, and a return expression in a function with a declared return type. The rule is the same everywhere — if the compiler knows what is expected there, it pushes that type inward.
  • If the extracted callback is left un-annotated and `noImplicitAny` is off, what happens?
    The parameter silently becomes `any`, the declaration compiles, and `nums.map(cb)` is accepted — but every use of that parameter is now unchecked, so `x.toUpperCase()` inside the body would pass type checking and blow up at runtime. That silent hole is exactly what `noImplicitAny` exists to surface, which is why the error is a feature rather than an inconvenience.

saying these in an interview costs you the question

  • Says the extracted callback is incompatible with map
  • Thinks the compiler looks ahead at later usage
  • Believes annotating the variable cannot type the parameter
  • Claims parameters default to unknown rather than any
  • Assumes inline arrows are typed because arrows are special

context