Function overloads

TypeScript · Advanced · 12 min

Overloads describe JavaScript APIs whose return type depends on the arguments, which many real libraries have.

Some functions return different types depending on how they're called. Overloads describe this by listing several overload signatures followed by one implementation signature:

function parse(input: string): number;
function parse(input: string[]): number[];
function parse(input: string | string[]): number | number[] {
  return Array.isArray(input) ? input.map(Number) : Number(input);
}

Callers see only the overload signatures — the implementation signature is hidden. So parse("1") is number and parse(["1"]) is number[], with no casts at the call site.

Rules worth knowing:

  • The implementation must be compatible with every overload, but the compiler checks this loosely: inside the body you still deal with the wide types and must return the right thing yourself. Overloads are a promise, not a proof.
  • Overloads are tried in order and the first match wins. Put more specific signatures before general ones, or a broad signature will swallow calls meant for a later one.
  • A call that only works through the combination of overloads fails. With the overloads above, parse(x) where x: string | string[] is an error, since neither single signature accepts the union. Add a third overload for the union if callers need it.

Prefer simpler tools when they suffice:

  • If only the parameter types vary and the return type doesn't, use a union parameter.
  • If signatures differ only by trailing parameters, use optional parameters.
  • If the output type follows the input type directly, use a generic.

Overloads are also available for methods in classes and interfaces (just list several method signatures) and for call signatures in object types.

Example

function createElement(tag: "a"): HTMLAnchorElement;
function createElement(tag: "img"): HTMLImageElement;
function createElement(tag: string): HTMLElement;
function createElement(tag: string): HTMLElement {
  return document.createElement(tag);
}
function parse(input: string): number;
function parse(input: readonly string[]): number[];
function parse(input: string | readonly string[]): number | number[] {
  return typeof input === "string" ? Number(input) : input.map(Number);
}
const n = parse("42");               // number
const ns = parse(["1", "2"]);        // number[]
declare const either: string | string[];
// @ts-expect-error -- No overload matches this call
parse(either);

if (typeof document !== "undefined") {
  const link = createElement("a");   // HTMLAnchorElement
  link.href = "/home";
}
console.log(n + 1, ns.length);

Remember

  • Overload signatures are visible to callers; the implementation signature is not
  • Order overloads from most specific to most general
  • A union argument won't match unless an overload accepts the union
  • Prefer unions, optional parameters or generics when they can express the API

Watch out for

  • Writing overloads that differ only in a trailing optional parameter — one signature with param? is simpler.

Check yourself

  1. Overloads: f(x: string): string; f(x: number): number; plus an implementation. What happens with f(v) where v: string | number?
  2. Why should a general overload like f(x: unknown): unknown come last?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: www.typescriptlang.org/docs/handbook/2/functions.html#function-overloads