In TypeScript, `['a', 'bb'].map(s => s.length)` compiles with no annotation on `s`, but `const len = (s) => s.length;` is an error under `noImplicitAny`. What is the difference between the two positions?
answer
- types travelling inward, not outward
- the position already knows what it expects
- matched by position, not by name
- declarations never receive it
- extract the arrow and it vanishes
basics
~20 sContextual typing. The callback sits in a position whose expected type is already known — the declared signature of the map callback — so its parameter takes its type from there. The standalone arrow has no expected type, so nothing constrains its parameter.
solid answer
~50 sThe difference is whether an expected type exists at the position where the function expression is written. `Array.prototype.map` declares its callback as `(value: T, index: number, array: T[]) => U`, so when you pass an arrow there the compiler matches the arrow's parameters against that signature positionally and gives `s` the type `string`. That is contextual typing, and it flows into the parameters and into the return position as well. The standalone `const len = (s) => s.length` has no contextual type at all — the declaration is the outermost thing there is, so `s` falls back to implicit `any` and `noImplicitAny` reports it. You fix it either by annotating the parameter, `(s: string) => s.length`, or by giving the variable a type so the context reappears: `const len: (s: string) => number = (s) => s.length`. Contextual typing only ever applies to function expressions and arrows, never to a function declaration.
code
typescript · 6 linesconst lengths = ["a", "bb"].map((s) => s.length);
type Compare = (a: number, b: number) => number;
const byValue: Compare = (a, b) => a - b;
console.log(lengths, byValue(1, 2));go deeper
Recognise that a callback passed directly to a method often needs no annotations, while a function you declare on its own always does, and be able to name that mechanism as contextual typing.
Explain the direction of flow: the expected type at the position is pushed into the function expression's parameters, matched positionally, and function declarations never receive it.
Diagnose the failure modes in real code — an extracted callback losing its context, an overloaded or union-typed target, a callback nested inside an object literal handed to a generic function — and know the fix is a restored annotation.
Own the convention: where shared function types are declared so call sites can lean on context instead of repeating annotations, and how that choice keeps a large codebase's annotations concentrated at real boundaries.
## Two directions in which types travel Most of the time you think of types flowing *outward*: you annotate the inputs, the compiler works out the result. Contextual typing is the inward flow. When the compiler already knows what type is expected at a position, it pushes that type down into the expression written there — and for a function expression, that means handing types to the parameters. ## What supplies the context The callback case is the one everybody meets first. In the standard library, `map` is declared roughly as: ```ts map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[]; ``` On `['a', 'bb']`, `T` is `string`. When you write `.map(s => s.length)`, the compiler already knows the argument's expected type is `(value: string, index: number, array: string[]) => U`, so it matches your parameters against that signature **by position** and types `s` as `string`. Your name for the parameter is irrelevant; only its position matters. You may also declare fewer parameters than the signature offers — `s => ...` ignoring `index` and `array` is fine — and the ones you do declare are typed from the front. Callback arguments are not the only source of context. All of these supply one: ```ts type Compare = (a: number, b: number) => number; const byValue: Compare = (a, b) => a - b; // from the variable's annotation const handlers: Record<string, (n: number) => void> = { log: (n) => console.log(n), // from the property type }; function pick(cmp: Compare) {} pick((a, b) => b - a); // from the parameter type ``` A type assertion, a `return` inside a function with an annotated return type, and the right-hand side of an assignment to an already-typed variable all supply context too. ## Why the standalone arrow gets nothing `const len = (s) => s.length;` has no annotation on `len`, so there is no expected type to push inward. The arrow is the source of truth, not the consumer of one. `s` therefore falls back to implicit `any`, `noImplicitAny` errors, and — worth noticing — the inferred type of `len` becomes `(s: any) => any`, so the hole would propagate to every caller if the flag were off. Two fixes, and they are genuinely different in intent: ```ts const len = (s: string) => s.length; // annotate the parameter const len2: (s: string) => number = (s) => s.length; // annotate the variable, reintroducing context ``` The first states the contract inline. The second is what you want when a named function type already exists: write it once on the variable and every parameter, plus the return type, is checked against it. ## Declarations never get context Contextual typing applies to function *expressions* and arrow functions. A function declaration is a statement, not an expression sitting in a typed position, so it never receives context — which is why the rule "annotate the parameters of your declarations" holds without exception, while callbacks routinely need no annotations at all. ## Where the context runs out Contextual typing is a real mechanism with real edges, and the failures look confusing until you know the cause: - **Extract the callback into a variable and the context disappears.** `const cb = (s) => s.length;` then `arr.map(cb)` — the arrow is now checked in isolation, with no expected type, so `s` is an implicit any. Annotate it, or type the variable. - **Overloaded targets.** When the parameter's declared type is an overloaded function type, contextual typing does not merge the overloads, and parameters can come out as something you did not expect. - **Union-typed positions.** If the expected type is a union of function types, the compiler may fail to pick one and leave the parameters implicitly any. - **Callbacks nested in an object literal passed to a generic function.** Inference for the type parameter may not have settled when the callback is checked, so the callback loses the context you expected it to have. In all of these the remedy is the same: put the annotation back, either on the parameter or on the variable holding the function. ## Why this is worth understanding, not just memorising The practical payoff is knowing which annotations are load-bearing. Annotating callback parameters that the context already types is noise, and worse, it can be silently *wrong* in a way an inferred parameter cannot — you can write `(s: string | undefined) => ...` where the context guarantees `string`, and that widened type sticks for the whole body. Leave contextual positions bare, annotate everything that has no context, and a codebase ends up with annotations exactly where information is actually being supplied rather than restated.
- Why can the callback declare fewer parameters than the signature it is checked against?Because a function that ignores arguments is safely usable where more are supplied — the caller passes three, yours reads one, the extras are discarded. TypeScript therefore treats a function type with fewer parameters as assignable to one with more, which is what makes `arr.map(s => ...)` legal against a three-parameter callback signature.
- You move an inline callback into a named const and it suddenly errors. What happened?The contextual type was lost. Inline, the arrow was checked against the callback's declared signature; extracted, it is checked on its own with nothing supplying parameter types, so they become implicit any. Fix it by annotating the parameters, or by typing the const with the function type the call site expects.
- Should you annotate callback parameters anyway, for readability?Usually not. The annotation is redundant when the context supplies it, and it can be quietly wrong or wider than reality — a hand-written `string | undefined` where the context guarantees `string` weakens the body for no gain. Rely on context in contextual positions; spend annotations where no context exists.
saying these in an interview costs you the question
- Thinks the callback parameter is inferred from the array's runtime values
- Believes parameter names must match the expected signature's names
- Expects a function declaration to get parameter types from its call sites
- Says annotating every callback parameter is always the safer habit
- Cannot explain why extracting a callback into a variable breaks it