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.
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
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 } = {}) { … }
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.
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
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
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.
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
function handle(this: HTMLButtonElement, event: MouseEvent) {
this.disabled = true;
}
button.addEventListener("click", handle);
Which is usually the point — and exactly why an arrow is the wrong shape for a method that needs one.
Functions that never return
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.
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
}