Skip to content

Functions

A function type describes parameters and a return type. You can write it inline or as a named type:

type BinaryOp = (a: number, b: number) => number;
const add: BinaryOp = (a, b) => a + b;
// a and b are inferred as number from the context — no annotations needed

Parameters support optional (?), default values, and rest:

function make(name: string, count = 1, ...tags: string[]): void {
// count defaults to 1; tags collects the rest into string[]
}

An optional parameter x?: number is number | undefined; a defaulted parameter count = 1 is simply number from the callee’s view, but callers may omit it.

Overloads — and why a union is often better

Section titled “Overloads — and why a union is often better”

Overloads let one function present multiple call signatures. You write several signature lines followed by one implementation:

function len(x: string): number;
function len(x: unknown[]): number;
function len(x: string | unknown[]): number {
return x.length;
}
len("hi"); // ✅ 2
len([1, 2, 3]); // ✅ 3

Overloads are powerful but easy to overuse. When the signatures differ only in a parameter type — not in the relationship between parameters and return — a single union parameter is simpler and clearer:

// Simpler than two overloads:
function len(x: string | unknown[]): number {
return x.length;
}

Reach for real overloads only when the return type genuinely depends on which input shape you got in a way a union can’t express.

In a standalone function, this can be typed with a special first parameter that disappears at the call site (it’s compile-time only):

interface Button { label: string; }
function handleClick(this: Button, event: Event): void {
console.log(this.label); // this is typed as Button
}

This catches the classic bug of a method losing its this when passed as a bare callback. It’s most useful in older this-based APIs; with arrow functions and modern patterns you’ll need it less.

The void return rule that surprises everyone

Section titled “The void return rule that surprises everyone”

A function type that returns void does not mean “the function must return nothing.” It means “the caller will ignore any return value.” So a function that does return something is still assignable to a void-returning type:

type Callback = () => void;
const cb: Callback = () => 42; // ✅ allowed! the 42 is just ignored
flowchart LR
  fn["() => number
(returns 42)"] -->|assignable to| target["() => void
(caller ignores return)"]
  target --> use["callers treat result
as void — safe"]
void means the return is ignored, not forbidden

This is exactly why array.forEach(x => arr.push(x)) type-checks even though push returns a number: forEach wants a void callback and simply ignores whatever comes back. Once you know the rule it stops being mysterious — but almost everyone hits it confused first.

When should you use overloads instead of a single union parameter?
What does a `this` parameter (`function f(this: T, ...)`) do at the call site?
Is `const cb: () => void = () => 42` allowed?
Why does `arr.forEach(x => other.push(x))` type-check even though push returns a number?