RizTech Academy logo
RizTech Academy
FunctionsLesson 4 of 425 min

Overloads, and why you usually want a union instead

Some functions genuinely do different things depending on their arguments — document.createElement returns a different element type for "a" than for "canvas". TypeScript can describe these with function overloads. This lesson covers them — and, more importantly, teaches you that you usually do not need them: a union type or generics is nearly always simpler. Knowing when overloads are the right tool, and when they are ceremony, is the real lesson.

What overloads look like

An overloaded function has several signatures describing how it can be called, followed by one implementation that handles them all:

// the overload signatures — how callers may invoke it
function parse(value: string): string[];
function parse(value: number): number[];
// the implementation signature — not visible to callers
function parse(value: string | number): string[] | number[] {
  if (typeof value === "string") return value.split("");
  return [value];
}

const a = parse("abc");    // TypeScript knows a is string[]
const b = parse(42);       // TypeScript knows b is number[]

The two signatures at the top are what callers see; the implementation below (with the union parameter) is not callable directly — it just implements the overloads. The payoff is precision: parse("abc") is typed string[] and parse(42) is typed number[], because each overload maps a specific input to a specific output. A single (value: string | number) => string[] | number[] signature could not do that — it would type both calls as string[] | number[], losing the connection.

Why you usually do not need them

Overloads look powerful, and beginners reach for them too often. In most cases a union parameter or generics is simpler and clearer.

Case 1: the function does the same thing regardless of type → use a union.

// overloaded (unnecessary):
function double(x: number): number;
function double(x: string): string;
function double(x: number | string): number | string {
  return typeof x === "number" ? x * 2 : x + x;
}

// simpler — a union parameter, same behaviour:
function double(x: number | string): number | string {
  return typeof x === "number" ? x * 2 : x + x;
}

If the input types do not need to map to specific different output types, the overloads add nothing — the union signature says the same thing with less code. Reach for overloads only when different inputs produce different, correlated outputs.

Case 2: the output type depends on the input type → use generics.

// overloaded:
function first(arr: string[]): string;
function first(arr: number[]): number;
function first(arr: any[]): any { return arr[0]; }

// far better — a generic, works for ANY element type, one signature:
function first<T>(arr: T[]): T {
  return arr[0];
}
const s = first(["a", "b"]);   // string
const n = first([1, 2]);       // number

A generic (the next module) captures "the output type is the input's element type" for every type in one clean signature — the overloaded version only covered string and number, and its implementation had to use any. When the return type is a function of the argument type, that is what generics are for, and they are almost always the right tool over overloads.

The genuinely good use of overloads

Overloads earn their place in one specific situation: when the input types map to different outputs in a way that is not a simple type transformation — where generics cannot express the relationship because it is a fixed, enumerated mapping rather than a parametric one. The classic real example is document.createElement:

createElement(tag: "a"): HTMLAnchorElement;
createElement(tag: "canvas"): HTMLCanvasElement;
createElement(tag: "div"): HTMLDivElement;
createElement(tag: string): HTMLElement;   // fallback

Here each specific string maps to a specific, unrelated element type — there is no formula relating "canvas" to HTMLCanvasElement, just a fixed correspondence. Generics cannot express "this exact string produces this exact type"; overloads (or a mapped/conditional type, advanced-types module) can. This — a fixed, hand-written mapping from specific input values to specific output types — is when overloads are the right tool, and it is genuinely uncommon in application code.

The costs, and the rule

Overloads have real downsides that reinforce "use them rarely":

  • The implementation signature is unchecked against the overloads in a loose way — TypeScript does not fully verify that your implementation correctly handles every overload, so a mismatch can slip through. You carry more responsibility for correctness.
  • They are verbose — several signatures plus an implementation, versus one union or generic signature.
  • They are easy to get subtly wrong — parameter compatibility between overloads and the implementation is fiddly.

The rule: reach for a union parameter first, generics second, and overloads only when you have a fixed mapping from specific input values to specific unrelated output types that neither can express. In months of application code you may write no overloads at all — and that is a sign you are choosing the simpler tools correctly, not a gap. When you see overloads (mostly in library type definitions like the DOM), you will now understand why they were used.

Check your work

What a function overload is. Several call signatures describing how a function may be invoked, plus one implementation that handles them all.

What overloads give you. Precision — a specific input type maps to a specific output type, which a single union signature cannot express.

Which tool for "same behaviour regardless of type". A union parameter — overloads add nothing.

Which tool for "output type depends on input type". Generics — they capture the relationship for every type in one signature, no any.

When overloads genuinely earn their place. A fixed, enumerated mapping from specific input values to specific unrelated output types (like createElement) that generics cannot express.

Three costs of overloads. Loose checking of the implementation, verbosity, and fiddly parameter compatibility.

The rule. Union parameter first, generics second, overloads only for a fixed specific-value-to-type mapping.

Practice

  1. Write the overloaded parse and confirm parse("abc") is string[] and parse(42) is number[].
  2. Rewrite a double(x: number | string) as a single union signature and confirm it is simpler.
  3. Take an overloaded first(string[])/first(number[]) and rewrite it as a generic first<T>. Confirm it works for a boolean[] too, which the overloads did not cover.
  4. Look up the real type of document.createElement in your editor (hover it) and note the overloads.
  5. Explain why a generic cannot express createElement's "canvas" -> HTMLCanvasElement mapping.
  6. For three functions you might overload, decide whether a union, a generic, or an overload is right, and justify each.

Official documentation

Next module — Unions and Narrowing: the part of TypeScript that makes it genuinely powerful.

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