TypeScript Function Overloads Explained
TypeScript function overloads let one function name accept multiple call signatures with different types. How overload signatures work and when to use them.
TypeScript function overloads let a single function name expose multiple call signatures, each with its own combination of parameter and return types, while a single implementation handles all of them at runtime. They exist because JavaScript only ever has one function body per name — TypeScript’s overloads are a purely type-level way of saying “this function behaves differently depending on what you pass it,” which a plain union-typed signature often can’t express precisely.
The problem overloads solve
Consider a function that looks up a record by either a numeric ID or a string slug, and returns a different shape depending on which was passed:
function find(id: number): { id: number; name: string };
function find(slug: string): { slug: string; name: string };
function find(query: number | string): { id?: number; slug?: string; name: string } {
if (typeof query === "number") {
return { id: query, name: "example" };
}
return { slug: query, name: "example" };
}
The first two lines are overload signatures — they’re the only signatures callers ever see. The third line is the implementation signature, which must be compatible with every overload but is never directly visible to code calling the function. Call find(42) and TypeScript reports the return type as { id: number; name: string }; call find("abc") and it reports { slug: string; name: string } — precision a single union-typed signature can’t give you, because a union return type would force every caller to narrow the result themselves regardless of which overload logically applies.
Overloads vs a union parameter
A simpler alternative is often just a union type:
function find(query: number | string): { name: string } { /* ... */ }
This works, but it loses the connection between input and output type. If the return type genuinely varies based on which input type was passed, a union-typed function forces the caller to re-narrow the result with an if check or a type assertion — exactly the kind of manual narrowing TypeScript’s inference is supposed to remove. Overloads push that connection into the type system itself, so find(42) and find("abc") each get exactly the return type they should, with zero narrowing required at the call site.
The tradeoff is verbosity. Overloads mean writing out every valid combination as a separate signature, and the implementation signature has to be broad enough to satisfy all of them, which usually means falling back to a union internally anyway. For a function with two or three genuinely distinct call shapes, that’s a reasonable cost. For a function that just accepts optional parameters or slightly different input types with the same output shape, a union parameter or generics is almost always simpler.
Overload resolution order
TypeScript checks a call against overload signatures top to bottom and uses the first one that matches. This means overload order matters — a more specific signature should come before a more general one, or the general one will shadow it and the specific one will never actually get selected:
function process(value: string): string;
function process(value: string | number): string; // shadows the line above
function process(value: string | number): string {
return String(value);
}
Here calling process("x") still matches the first, more specific signature because it comes first — but if the order were reversed, the second signature would always win and the first would be unreachable. Keep the narrowest, most specific overloads first.
When to reach for overloads instead of alternatives
Overloads make the most sense when:
- A function’s return type genuinely depends on which input type was passed, not just on the input’s value
- The number of distinct call shapes is small — beyond four or five, overloads become hard to read and maintain
- You’re typing a function whose call shapes can’t be expressed cleanly with a discriminated union on a single parameter
They make less sense when a discriminated union on an options object would do the same job more clearly — passing { type: "byId", id: number } versus { type: "bySlug", slug: string } is often easier to read and extend than a growing list of overload signatures, and it avoids the overload-order pitfall entirely. It’s also worth deciding early whether a parameter should be typed as unknown or any — overloads only add value when the input types are meaningfully distinct, not when the function is deliberately permissive.
Overloads on methods and constructors
Overload signatures aren’t limited to standalone functions — class methods and constructors can be overloaded the same way, which is common in library code where a class needs to be instantiated from a few genuinely different shapes of input:
class Point {
constructor(x: number, y: number);
constructor(coords: [number, number]);
constructor(xOrCoords: number | [number, number], y?: number) {
// implementation handles both shapes
}
}
The same ordering rule applies here as with standalone functions: list the more specific constructor signature first if there’s any overlap in what a caller’s arguments could match. This pattern shows up often in DOM APIs and utility libraries, where a constructor or factory function is designed to accept either a set of primitive arguments or a single pre-assembled object.
The takeaway
Function overloads give TypeScript a way to describe a function whose return type depends on which of several call shapes was used, something a single union-typed signature can’t express without pushing narrowing work onto every caller. They’re a precision tool for functions with genuinely distinct call signatures — order the overloads from most to least specific, keep the implementation signature private to the type checker, and reach for a discriminated union or generics instead when the call shapes are close enough that overloads would just add ceremony.
Keep reading
Takina · · 4 min read TypeScript Abstract Classes, Explained
Abstract classes in TypeScript define shared implementation plus methods subclasses must fill in. How they differ from interfaces and when to reach for them.
Takina · · 3 min read TypeScript's never Type, Explained
never represents values that can't exist — it marks unreachable code, exhaustive switches, and functions that always throw or loop forever.
Takina · · 4 min read TypeScript readonly Modifiers Explained
TypeScript's readonly keyword blocks reassignment at compile time for properties, arrays, and tuples — with no runtime enforcement at all.