Function types and callbacks
In TypeScript, a function is a value — you can store it in a variable, pass it to another function,
and return it. To do that with types, you need to describe the type of a function: what it takes and
what it returns. This lesson is the function type, and the callbacks and higher-order functions
built on it — the machinery behind map, filter, event handlers, and most modern TypeScript.
The function type syntax
A function type is written with an arrow: (params) => returnType.
let greet: (name: string) => string;
greet = (name) => `Namaste, ${name}`; // fine — matches the type
greet = (name) => name.length; // error: number is not assignable to string return
greet = () => "hi"; // error: too few parameters... (actually allowed — see below)
(name: string) => string is the type "a function taking a string and returning a string". Notice that
when you assign a function whose type is already known, you do not re-annotate the parameters —
(name) => ... infers name is a string from the declared function type. This is contextual
typing: TypeScript reads the expected type and fills in the parameter types for you. It is why the
lambdas in arr.map(x => x * 2) need no annotation — the map method's type tells TypeScript what
x is.
Typing a callback parameter
The most common use of a function type is a callback — a function you pass to another function:
function repeat(times: number, action: (index: number) => void): void {
for (let i = 0; i < times; i++) {
action(i);
}
}
repeat(3, (i) => console.log(`Step ${i}`)); // i inferred as number
action: (index: number) => void types the callback: it takes a number and returns nothing useful.
The caller passes a lambda, and TypeScript infers i is a number from the callback type — no
annotation needed at the call site. This is exactly how the standard library types map, filter,
forEach, setTimeout, and every event handler.
You can name a callback type for reuse (and clarity) with a type alias:
type ClickHandler = (event: { x: number; y: number }) => void;
function onClick(handler: ClickHandler) { /* ... */ }
onClick((e) => console.log(e.x, e.y)); // e is typed from ClickHandler
The standard collection methods, typed
Now the collection operations make full sense — they are higher-order functions whose callback types drive the inference:
const prices = [120, 340, 90];
const doubled = prices.map((p) => p * 2); // map's callback is (p: number) => U; result number[]
const cheap = prices.filter((p) => p < 200); // filter's callback is (p: number) => boolean
const total = prices.reduce((sum, p) => sum + p, 0); // reduce's callback is (acc, p) => acc
const names = prices.map((p) => `₹${p}`); // callback returns string, so result is string[]
In every case, TypeScript infers the callback parameter types (p is number) from the array's
element type, and infers the result type from what the callback returns (map returning a string
gives string[]). You write clean, un-annotated lambdas and get full type safety — because the
function types on map/filter/reduce carry all the information.
Returning a function
A function can return a function, and you type the returned function the same way:
function multiplier(factor: number): (n: number) => number {
return (n) => n * factor; // n inferred number from the declared return type
}
const triple = multiplier(3); // triple has type (n: number) => number
triple(10); // 30
multiplier returns a (n: number) => number. The returned lambda needs no parameter annotation
because the return type tells TypeScript what n is. This is how you write factories and configurable
functions with full type safety.
Function-type compatibility — the surprising rules
Two rules about when one function type fits another catch people, and both are about safety:
A function with fewer parameters is assignable where more are expected. This seems backwards but is safe:
type Callback = (index: number, value: string) => void;
const cb: Callback = (index) => console.log(index); // fine — ignores `value`
A callback that ignores some arguments is fine — the caller passes both, the function uses one, no harm.
This is why arr.forEach(x => ...) works even though forEach's callback also receives the index and
the array — you just ignore the ones you do not need.
Return types are checked, and void is special. A function returning a value fits where void is
expected (the return is ignored), but a function returning void does not fit where a specific return
is expected. This asymmetry (met in the typing-functions lesson) is why arr.forEach(() => doSomething())
is permissive. For most work you do not think about these rules — they are tuned so the common,
intuitive cases just work — but knowing they exist explains a few otherwise-baffling "why does this
compile?" moments.
Check your work
How a function type is written. (params) => returnType — an arrow between the parameters and the
return type.
What contextual typing does. When the expected function type is known, TypeScript infers the
parameter types, so lambdas need no annotation (arr.map(x => ...)).
How to type a callback. As a parameter of function type — action: (index: number) => void.
How the collection methods are typed. As higher-order functions whose callback types drive
inference — p is inferred from the element type, the result from the callback's return.
How to type a returned function. With a function-type return annotation; the returned lambda's parameters infer from it.
The fewer-parameters rule. A function with fewer parameters fits where more are expected — ignoring
extra arguments is safe (why forEach(x => ...) works).
The void return rule. A function returning a value fits where void is expected; the reverse does
not.
Practice
- Declare a variable of type
(name: string) => stringand assign a matching lambda. Assign one with the wrong return type and read the error. - Assign
(name) => ...(no annotation) and confirmnameis inferred asstringfrom the declared type. - Write
repeat(times, action: (i: number) => void)and call it with a lambda. Confirmiis inferred. - Name a callback type with a type alias and use it in a function parameter.
- Chain
map,filter, andreduceon anumber[]with un-annotated lambdas. Confirm each callback parameter and the result type. - Write
multiplier(factor): (n: number) => numberand use its result. - Pass a one-parameter lambda to
forEach(which offers three arguments) and confirm it is allowed. Explain why via the fewer-parameters rule.
Official documentation
- TypeScript — Function Type Expressions — The
(x) => ysyntax. - TypeScript — Contextual Typing — How lambda parameters get inferred.
- TypeScript — Assignability of functions — The parameter and return rules.
Next: overloads, and why you usually want a union instead.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship