C.W.K.
Stream
Lesson 05 of 05 · published

Contextual Typing: When TypeScript Reads Your Callsite

~10 min · functions, contextual-typing, inference, callbacks

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
"You don't have to tell TypeScript what parameters mean when the call site already does."

What contextual typing is

When a function is in a position where the surrounding code already declares the function type — e.g., the callback parameter of arr.map(...) is known to be (x: T, i: number) => U — TypeScript uses that declaration to type the function's parameters automatically. You don't have to annotate (x); TypeScript already knows what x is from the context.

This is the reason you can write arr.map(x => x.length) without ever specifying that x is a string. The compiler reads arr's type (say string[]), looks up .map's signature, sees that the callback takes a string, and types x accordingly. The arrow function inherits its parameter types from the slot it's filling.

Where contextual typing happens

  • Function arguments: arr.filter(x => x > 0)x typed from the array element type.
  • Object property assignments to typed slots: const cb: (x: number) => void = (x) => ...x is number.
  • Right side of typed variable declarations: const f: Reducer<User> = (acc, u) => ... — both params typed from the alias.
  • Return-position function shapes: when a function returns another function and the outer return type is declared, the inner function's params are typed.

Why it matters

Contextual typing is what makes TypeScript's callback ergonomics feel natural. Without it, you'd have to annotate every callback parameter — arr.map((x: string) => x.toUpperCase()) — which is repetitive and fragile (rename the array's element type and you'd have to update every callback). With it, callbacks adopt their parameter types from the API they're passed to. The result is concise, correct, and refactor-friendly.

The rule: if the slot a function is going into already has a function type, don't annotate the parameters again. Let inference fill them in. The exception is when inference picks something too wide or too narrow — there, add the annotation to override.

When contextual typing fails

Contextual typing requires the surrounding context to already declare the function type. A standalone arrow function in a let binding without an annotation can't infer parameter types — there's nothing for them to be inferred from. The fix is to annotate the variable (or use it in a typed position).

const f = (x) => x + 1;   // ❌ Parameter 'x' implicitly has 'any' type
const g: (x: number) => number = (x) => x + 1;   // ✅ contextual from the variable's type
[1, 2].map((x) => x + 1);                          // ✅ contextual from the array

Pippa's confession

cwkPippa's frontend has thousands of arrow callbacks, almost none of which annotate their parameters. The compiler reads the array, the React event handler, the hook's setter, the Promise's .then — and types every callback parameter automatically. The result is readable code that the type system still understands. Every time a beginner annotates a callback parameter that didn't need it, I gently delete it. Inference does this better than you do.

Code

Contextual typing — params come from the slot·typescript
// Contextual typing in action.

const names: string[] = ['Pippa', 'Dad', 'Ttori'];

// All three callbacks have parameters typed automatically:
const upper = names.map((n) => n.toUpperCase());      // n: string
const long = names.filter((n) => n.length > 3);       // n: string
const sumChars = names.reduce((acc, n) => acc + n.length, 0);  // acc: number, n: string

// React-style event handler — same idea.
document.body.addEventListener('click', (e) => {
  // e: MouseEvent — pulled from addEventListener's signature
  e.preventDefault();
});

// Variable with a function type — params from the alias.
type Reducer<T> = (acc: T, x: T) => T;
const sumPair: Reducer<number> = (a, b) => a + b;     // a: number, b: number
Where contextual typing fails — and how to recover·typescript
// When it fails — and the fixes.

// Fail: no context, parameter implicit any.
const broken = (x) => x + 1;          // ❌ Parameter 'x' implicitly has 'any' type

// Fix 1: annotate the parameter.
const withAnnotation = (x: number) => x + 1;

// Fix 2: annotate the variable.
const withVarType: (x: number) => number = (x) => x + 1;

// Fix 3: use it in a typed position.
const nums = [1, 2, 3];
const doubled = nums.map((x) => x * 2);  // x typed from array — no annotation needed

// Avoid the temptation to over-annotate.
const overkill = nums.map((x: number, i: number) => x + i);  // params already known; annotations are noise

External links

Exercise

Take three different callback APIs from your codebase or a library: Array.map, addEventListener, and Promise.then. For each, write an inline callback that uses its parameters meaningfully. Don't annotate any parameter. Confirm via hover that TypeScript knows the type. Then pull one of those callbacks out into a named function — what changes?
Hint
Inline: contextual typing applies, no annotations needed. Named-and-then-passed: the named function has to be annotated independently, because at its definition site there's no context to read from.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.