RizTech Academy logo
RizTech Academy
FunctionsLesson 3 of 425 min

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

  1. Declare a variable of type (name: string) => string and assign a matching lambda. Assign one with the wrong return type and read the error.
  2. Assign (name) => ... (no annotation) and confirm name is inferred as string from the declared type.
  3. Write repeat(times, action: (i: number) => void) and call it with a lambda. Confirm i is inferred.
  4. Name a callback type with a type alias and use it in a function parameter.
  5. Chain map, filter, and reduce on a number[] with un-annotated lambdas. Confirm each callback parameter and the result type.
  6. Write multiplier(factor): (n: number) => number and use its result.
  7. 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

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