Functions

Type what goes in and what comes out — and know where an annotation earns its place.

06of 14
≈ 12Min read
8Parts

Parameters always, returns usually

Parameters must be annotated — there is nothing to infer them from. The return type is inferred, and writing it anyway means a mistake inside the function is reported there rather than at every call site.

the return type is inferred, but writing it is a promise
function total(doors: Door[]): number {
  return doors.reduce((n, d) => n + d.price, 0);
}

const label = (door: Door): string => `${door.name} — ${door.days} days`;

Optional, default and rest

the same three JavaScript has
function order(door: Door, quantity = 1, note?: string) { … }

function sum(...numbers: number[]): number { … }

// an options object is usually kinder than four optional parameters
function render(doors: Door[], options: { sort?: Sort; limit?: number } = {}) { … }
Optional parameters come last

A required parameter after an optional one is an error. When the order will not bend, take an options object instead.

Function types

This is contextual typing: because the parameter has a type, the arrow you pass does not need annotations. It is why map((d) => …) needs nothing written on d.

describing a callback
type Compare = (a: Door, b: Door) => number;
type Handler = (event: MouseEvent) => void;

function sortBy(doors: Door[], compare: Compare): Door[] { … }

sortBy(doors, (a, b) => a.price - b.price);   // the parameters are inferred from Compare

void, undefined and the return you ignore

void is more permissive than it looks
type Handler = () => void;

const handler: Handler = () => doors.push(door);   // fine: the number is ignored

function nothing(): undefined { return undefined; }   // must return
function alsoNothing(): void { }                      // need not
Why that matters

arr.forEach(x => list.push(x)) type-checks because forEach takes a void callback and the returned number is discarded. If a function genuinely must return nothing, undefined is the stricter type.

Overloads

Without overloads the return would be the whole union at every call. Reach for them only when the return type genuinely depends on the argument — a union parameter is simpler when it does not.

one implementation, several honest signatures
function find(id: string): Door | undefined;
function find(ids: string[]): Door[];
function find(input: string | string[]): Door | Door[] | undefined {
  return Array.isArray(input) ? doors.filter(…) : doors.find(…);
}

const one = find("a");      // Door | undefined
const many = find(["a"]);   // Door[]

this, when you need it

a fake first parameter, erased at compile time
function handle(this: HTMLButtonElement, event: MouseEvent) {
  this.disabled = true;
}

button.addEventListener("click", handle);
An arrow has no this

Which is usually the point — and exactly why an arrow is the wrong shape for a method that needs one.

Functions that never return

never, and why it is useful
function fail(message: string): never {
  throw new Error(message);
}

const door = doors.find((d) => d.id === id) ?? fail("No such door");
// door is Door — not Door | undefined, because fail() cannot return

Async functions

Write Promise<Door>, not Door. And remember what the assertion on the last line is worth: nothing, unless the response is validated.

the return type is always a promise
async function load(id: string): Promise<Door> {
  const res = await fetch(`/api/doors/${id}`);
  if (!res.ok) throw new Error(`HTTP ${res.status}`);

  return res.json() as Promise<Door>;   // json() is any: this is an assertion, not a check
}

Practice