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
- Write the overloaded
parseand confirmparse("abc")isstring[]andparse(42)isnumber[]. - Rewrite a
double(x: number | string)as a single union signature and confirm it is simpler. - Take an overloaded
first(string[])/first(number[])and rewrite it as a genericfirst<T>. Confirm it works for aboolean[]too, which the overloads did not cover. - Look up the real type of
document.createElementin your editor (hover it) and note the overloads. - Explain why a generic cannot express
createElement's"canvas" -> HTMLCanvasElementmapping. - For three functions you might overload, decide whether a union, a generic, or an overload is right, and justify each.
Official documentation
- TypeScript — Function Overloads — Signatures and the implementation.
- TypeScript — Overloads versus unions — Guidance on when to prefer a union.
- TypeScript — Generics — The tool for input-dependent output types (next module).
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