Topic 2.5
Function Overloads
In one line
An overloaded function has several allowed call shapes, each with its own result type, backed by one implementation. TypeScript picks the matching shape for each call, and often a union parameter is the simpler choice.
Think of it like this
A shop counter with two signs: "Give a name, get your parcel" and "Give a ticket number, get your receipt". Both go to the same window and the same clerk, but what you hand over decides what you receive. Overloads are those signs: several public ways to call one function, each with its own promise about the result.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Overload
- One of several allowed call shapes for the same function name.
- Overload signature
- A line like
function f(x: string): number;with no body. Callers see these. - Implementation signature
- The final declaration with the body. It must accept everything the overloads allow, but callers can't see it.
- Union type
- A type that allows one of several types, written
A | B. - Compatible
- Fits together without breaking either side's promise.
- Specific vs general
- A specific signature accepts a narrow set of values (
string), a general one accepts a wide set (unknown). - Call shape
- The combination of argument count and types that a call uses.
Step by step
01The problem a union can't solve
A function that turns a string into letters and a number into a one-item array has a result that depends on the input. With a union parameter the result type is the union of both results, and callers must check it every time.
02Write the overload signatures
One line per shape, no body, each followed by ;. They go directly above the implementation.
A call like toArray("abc") now has the type string[] and toArray(7) has number[].
function toArray(value: string): string[];
function toArray(value: number): number[];03Write the implementation
The implementation signature is wider: string | number in, a union out. It is not visible to callers.
Inside, you narrow with typeof (Topic 4.3) to decide what to do.
function toArray(value: string | number): (string | number)[] {
return typeof value === "string" ? value.split("") : [value];
}04The order of overloads
TypeScript tries them from the top and takes the first match. Put narrow shapes first; put a catch-all last. If the catch-all is first, it always wins.
05Prefer a union when the result doesn't change
size(x: string | unknown[]): number has one signature and one body. It accepts a string, an array, or a value of the union type. Overloads for the same job would reject the union argument.
Try it yourself
- 1
Swap the order
In the second example move the
unknownoverload ofpickbelow thestringone. Predict the type ofabefore you check. It becomesnumber, because the specific signature is now tried first. - 2
Call it with something unlisted
In the first example add
toArray(true);. Read the error. How many overloads does it mention, and what does it say about each? (TypeScript 7 mentions only the last one.) - 3
Pass a union to an overloaded function
In the first example write
declare const either: string | number;and calltoArray(either). Predict the outcome, then read it:TS2769, because the union fits neither overload alone. Now add a third overload linefunction toArray(value: string | number): (string | number)[];directly below the other two overloads (above the implementation) and call again. The error is gone and the result is(string | number)[]. What did you give up for that?
Code & diagrams
Callers see only the two signatures, so letters is string[] and single is number[]. Try toArray(true): the checker reports that no overload matches.
Console
(nothing was printed)Expected console output
[ 'a', 'b', 'c' ] [ 7 ]Both versions accept pick("hello"), but the result type differs. In the first, the catch-all unknown signature comes first and wins, so a is string even though the function returns 5. In the second, the specific string signature is first and b is number.
Console
(nothing was printed)One signature handles a string, an array, and a variable whose type is the union (describe). With two overloads (string) and (unknown[]), describe would not compile: a union argument matches neither one alone.
Console
(nothing was printed)Expected console output
3 2 5The hover shows the first overload and (+1 overload). The variable fn keeps both signatures as a type with two call signatures, which you'll write yourself in Topic 2.7. Note that here a single signature with height?: number would do the same job.
Console
(nothing was printed)Expected console output
9 12Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Call the hidden implementation signature
At the bottom of the first example add four lines: the only overload function parse(value: string): number;, then the implementation function parse(value: string | number): number {, its body return Number(value);, and }. On the line after that call parse(5);.
Break #2
An overload the implementation can't back up
At the bottom of the first example (without the parse lines) add the overload function wrap(x: number): number; and, right below it, the implementation function wrap(x: number, y: number): number { with the body return x + y; and }.
Myth vs fact
Myth
TypeScript checks each overload against the function body.
Fact
It checks the body only against the broad implementation signature. Nothing stops the body from returning a number for the string overload. You must make sure the promise is true.
Myth
Callers can use the implementation signature too.
Fact
It is invisible to callers. Only the overload signatures above it count.
Myth
Overloads are how TypeScript does what Java and C# do.
Fact
In Java and C# every overload is a separate method with its own body. In TypeScript there is exactly one function at runtime; the overloads are only different views of it for the type checker.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Overload resolution picks the first matching signature, but if an argument is a context-sensitive function (an arrow with no annotations), TypeScript may need several passes. Many overloads with callbacks slow down the checker and make error messages long. TypeScript 7 shortens the message by reporting only the last overload's failure.
- ▸
Overloads can't be expressed in an object literal method shorthand directly, but they can in a type:
{ (x: string): string[]; (x: number): number[] }, an object type with two call signatures (Topic 2.7). Interfaces can also declare overloaded methods by repeating the method name. - ▸
A generic with a conditional return type often replaces a family of overloads (Topic 9.1), but the implementation then needs a cast because the checker can't relate the body to the conditional type. Overloads stay the practical choice when the body is a few branches.
- ▸
Because the implementation signature is hidden, adding an overload for a new call shape is a compatible change for callers (earlier calls still resolve to the same overloads), as long as you put it after existing ones that could match.
Remember this
- 1
The problem this solves: One function that returns different types for different inputs gives a vague union result, so callers must check what they got even when the input already told them. Some JavaScript functions behave differently depending on their arguments:
toArray("abc")splits text into letters,toArray(7)wraps a number. One signature with a union (string | number) would lose the link between input and output, because the result would bestring[] | number[]either way. An overload keeps the link: string in,string[]out; number in,number[]out. - 2
You write an overloaded function in two parts. First the overload signatures: one line per allowed shape, each ending in
;and having no body. Then the implementation signature with the body, which must be general enough to handle all the overloads:function toArray(value: string | number): (string | number)[] { ... }. Callers only see the overload signatures; the implementation signature is hidden and cannot be called directly. - 3
That hiding is the part people trip over. If the overloads list
stringandnumberbut the implementation acceptsstring | number | boolean, you still can't call it with a boolean. A call must match one overload, or you getTS2769: No overload matches this call. If only one overload exists, the error is the ordinaryTS2345about the argument. TypeScript 6 prints one block for each overload that was tried; TypeScript 7 prints only the last overload's reason, so the exact lines differ, but the first line (the error code) is the same. - 4
Order matters. For each call TypeScript tries the overloads top to bottom and picks the first that matches. So list the most specific signatures first, and the most general last. If a catch-all like
(value: unknown): stringcomes first, it swallows everything and the specific ones below it never get used:pick("hello")would have the wrong result type. The live example shows this with a real hover. - 5
The implementation signature has to be compatible with every overload: a call that fits an overload must also fit the implementation, and the implementation's return type must cover each overload's return type. If it doesn't, you get
TS2394: This overload signature is not compatible with its implementation signature. Also, the overloads must sit directly above the implementation: a gap givesTS2391: Function implementation is missing or not immediately following the declaration. - 6
Overloads are checked only at the signature level. The body is checked against the broad implementation signature, not against each overload. So TypeScript will not notice if the body returns a number for the
stringoverload. Overloads are a promise you make, and you must keep it yourself. This is a common source of bugs, and one reason to prefer simpler alternatives. - 7
When is a union better? If the return type does not depend on which argument you pass, use a union parameter:
function size(x: string | unknown[]): number. It is one signature, the body is checked, and callers holding a union value (string | unknown[]) can call it. An overloaded version rejects that call, because a union argument matches neither overload on its own (TS2769). Use overloads only when the result type or the allowed combination of parameters really differs. Often a generic function (Phase 7) does the same job better.
Explain it without notes
What is a function overload, and how is it written?
Why is the implementation signature not callable?
Why does the order of overload signatures matter?
When would you choose a union parameter or a generic over overloads?
Practice
Write overloads for function double: a string input returns the string repeated twice, a number input returns the number doubled. Check the type of double("ab") and double(4) with // ^?.
Write overloads for function pad(text: string, width: number): string and function pad(text: string, width: number, char: string): string. Then rewrite it as a single signature with an optional parameter. Which is better?
A function len(x: string): number and len(x: number[]): number cannot be called with a string | number[] variable. Rewrite it so it can.
Trade-offs
- ↔
Overloads give callers precise result types, but they add duplicated signatures, a body that isn't checked against them, and long error messages. Use them for a few clearly different call shapes.
- ↔
A union parameter is easy and honest, but the result type loses the link to the input. If callers have to narrow the result every time, an overload or a generic is worth the extra lines.
Done when you can
I can write overload signatures plus an implementation signature.
I can explain why callers can't use the implementation signature.
I can explain how the order of overloads decides which one is picked.
I can read
TS2769andTS2394.I can decide between overloads, a union parameter and a generic.