Command Palette

Search for a command to run...

Hectal
PHASE 2Beginner ~26 min· topic 5 of 7

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[].

overloads.tswhole filets
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.

overloads.tswhole filets
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.

The order of overloadsdiagram
Rendering diagram…

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. 1

    Swap the order

    In the second example move the unknown overload of pick below the string one. Predict the type of a before you check. It becomes number, because the specific signature is now tried first.

  2. 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. 3

    Pass a union to an overloaded function

    In the first example write declare const either: string | number; and call toArray(either). Predict the outcome, then read it: TS2769, because the union fits neither overload alone. Now add a third overload line function 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

Overloads link the argument to the result

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 ]
Order matters: the first match wins

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)
A union parameter instead of overloads

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 5
Overloads with an optional parameter, and how they look

The 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 12

Break 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);.

terminal
$ Types panel
── what you'll see ──
index.ts(6,7): error TS2345: Argument of type 'number' is not assignable to parameter of type 'string'.

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 }.

terminal
$ Types panel
── what you'll see ──
index.ts(11,10): error TS2394: This overload signature is not compatible with its implementation signature.

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. 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 be string[] | number[] either way. An overload keeps the link: string in, string[] out; number in, number[] out.

  2. 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. 3

    That hiding is the part people trip over. If the overloads list string and number but the implementation accepts string | number | boolean, you still can't call it with a boolean. A call must match one overload, or you get TS2769: No overload matches this call. If only one overload exists, the error is the ordinary TS2345 about 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. 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): string comes 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. 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 gives TS2391: Function implementation is missing or not immediately following the declaration.

  6. 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 string overload. 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. 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

01

What is a function overload, and how is it written?

02

Why is the implementation signature not callable?

03

Why does the order of overload signatures matter?

04

When would you choose a union parameter or a generic over overloads?

Practice

01

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 // ^?.

02

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?

03

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 TS2769 and TS2394.

  • I can decide between overloads, a union parameter and a generic.