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?
answer
- type information can flow inwards
- the expected type at that position
- a standalone const has no expectation
- implicit any under noImplicitAny
- annotate the parameter or the variable
basics
~20 sA 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 sType 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 linesconst 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
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.
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.
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.
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