|$ curl https://forge-ai.dev/api/markdown?path=docs/typescript/overloads
$cat docs/typescript-—-function-overloads.md
updated Recently·10 min read·published

TypeScript — Function Overloads

TypeScriptIntermediate🎯Free Tools
Introduction

Function overloads let a single function accept different argument combinations and return different types depending on the inputs. They are useful when the return type depends on which arguments are passed.

Overload Signatures

Each overload is a separate function signature declared before the implementation. Callers see only the overload signatures, never the implementation signature.

signatures.ts
TypeScript
1// Overload signatures — visible to callers
2function format(value: string): string;
3function format(value: number): string;
4function format(value: Date): string;
5
6// Implementation signature — NOT visible to callers
7function format(value: string | number | Date): string {
8 if (typeof value === "string") return value.trim();
9 if (typeof value === "number") return value.toFixed(2);
10 return value.toISOString().slice(0, 10);
11}
12
13format(" hello "); // "hello" — string overload
14format(3.14159); // "3.14" — number overload
15format(new Date()); // "2024-01-15" — Date overload
Implementation Signature

The implementation signature must be compatible with all overload signatures. It is not callable directly — it only serves as the implementation body.

implementation.ts
TypeScript
1// ❌ Error: implementation must be compatible with overloads
2function bad(a: number): string;
3function bad(a: string): number;
4function bad(a: number | string): string | number {
5 if (typeof a === "number") return String(a);
6 return parseInt(a);
7}
8// bad(42) returns string — but overload says a: number returns string (OK)
9// bad("7") returns number — but overload says a: string returns number (OK)
10
11// ✅ The implementation parameter is usually a union of all overload types
12function createElement(tag: "div"): HTMLDivElement;
13function createElement(tag: "span"): HTMLSpanElement;
14function createElement(tag: "input"): HTMLInputElement;
15function createElement(tag: string): HTMLElement {
16 return document.createElement(tag);
17}
18
19// The implementation can return a wider type than any single overload
20// (but the caller sees the narrowed type from the matching overload)
Overload Resolution Order

TypeScript resolves overloads top-to-bottom, picking the first matching signature. This means ordering matters — place more specific overloads before more general ones.

resolution.ts
TypeScript
1// ✅ Correct: specific first, general last
2function parse(input: string): object;
3function parse(input: ArrayBuffer): object;
4function parse(input: string | ArrayBuffer): object {
5 if (typeof input === "string") {
6 return JSON.parse(input);
7 }
8 const decoder = new TextDecoder();
9 return JSON.parse(decoder.decode(input));
10}
11
12// ❌ Wrong order: general first — specific overloads never match
13function broken(input: string | ArrayBuffer): object;
14function broken(input: string): object; // ← unreachable!
15function broken(input: string | ArrayBuffer): object {
16 if (typeof input === "string") return JSON.parse(input);
17 const decoder = new TextDecoder();
18 return JSON.parse(decoder.decode(input));
19}
20// broken("hello") resolves to the first (general) overload
21// The specific string overload is never reached
22
23// Resolution picks the first match:
24function handle(x: string): string;
25function handle(x: object): object;
26function handle(x: unknown): unknown {
27 return x;
28}
29
30handle("hi"); // first overload: string → string
31handle({}); // second overload: object → object

warning

Always place the most specific overloads first. A general overload at the top will shadow all specific ones below it.
Common Patterns
patterns.ts
TypeScript
1// Pattern 1: Varying parameter count
2function createArray(): number[];
3function createArray<T>(value: T, count: number): T[];
4function createArray<T>(value?: T, count?: number): T[] {
5 if (value === undefined) return [];
6 return Array(count ?? 1).fill(value);
7}
8
9createArray(); // number[]
10createArray("a", 3); // string[]
11
12// Pattern 2: Overloaded factory
13interface User {
14 kind: "user";
15 name: string;
16 email: string;
17}
18interface Admin {
19 kind: "admin";
20 name: string;
21 email: string;
22 permissions: string[];
23}
24
25function createUser(name: string, email: string): User;
26function createUser(
27 name: string,
28 email: string,
29 permissions: string[]
30): Admin;
31function createUser(
32 name: string,
33 email: string,
34 permissions?: string[]
35): User | Admin {
36 if (permissions) {
37 return { kind: "admin", name, email, permissions };
38 }
39 return { kind: "user", name, email };
40}
41
42const u = createUser("Alice", "a@b.com"); // User
43const a = createUser("Bob", "b@b.com", ["all"]); // Admin
44
45// Pattern 3: Type narrowing via overloads
46function first<T>(arr: T[]): T;
47function first<T>(arr: readonly T[]): T;
48function first(arr: unknown[]): unknown {
49 return arr[0];
50}
Overloads vs Union Types vs Conditional Types
vs_others.ts
TypeScript
1// Approach 1: Overloads — explicit signatures
2function merge(a: string, b: string): string;
3function merge(a: number, b: number): number;
4function merge(a: string | number, b: string | number): string | number {
5 if (typeof a === "string") return a + b;
6 return (a as number) + (b as number);
7}
8
9// Approach 2: Union types — simpler but less precise
10function mergeUnion(a: string, b: string): string;
11function mergeUnion(a: number, b: number): number;
12function mergeUnion(a: string | number, b: string | number): string | number {
13 if (typeof a === "string") return a + (b as string);
14 return (a as number) + (b as number);
15}
16
17// Approach 3: Conditional types — compile-time only
18type MergeResult<A, B> = A extends string
19 ? B extends string
20 ? string
21 : never
22 : A extends number
23 ? B extends number
24 ? number
25 : never
26 : never;
27
28function mergeTyped<A extends string | number, B extends string | number>(
29 a: A,
30 b: B
31): MergeResult<A, B> {
32 if (typeof a === "string") return (a + (b as string)) as MergeResult<A, B>;
33 return ((a as number) + (b as number)) as MergeResult<A, B>;
34}
35
36// When to use which:
37// Overloads: when argument count or patterns vary, need runtime checks
38// Union types: when you want a simpler API and mixed types are OK
39// Conditional: when the type relationship is purely compile-time

info

Use overloads when the return type must narrow based on specific argument combinations. If a union return type is acceptable, prefer a simpler function signature.
Best Practices

1. Place the most specific overload signatures first — TypeScript resolves top-to-bottom and stops at the first match.

2. Limit overloads to 2–3 signatures. If you need more, consider generics, conditional types, or a builder pattern instead.

3. Keep the implementation signature as a union of all overload parameters — it must satisfy every overload.

4. Avoid overloads that only differ by return type — TypeScript can't distinguish them based on usage alone.

5. Use overloads for public APIs where type safety matters. Internally, unions or conditional types may be simpler.

6. Document each overload signature with a JSDoc comment describing when it applies.

Call Signature Overloads on Interfaces

Interfaces can declare multiple call signatures — useful for callable objects and libraries that mirror overloaded functions.

call-sigs.ts
TypeScript
1interface Parser {
2 (input: string): string[];
3 (input: string[]): string[];
4 (input: string | string[]): string[];
5}
6
7const parse: Parser = (input) =>
8 typeof input === "string" ? input.split(",") : input.map(String);
Constructor Overloads
ctor.ts
TypeScript
1class Point {
2 x: number;
3 y: number;
4 constructor(x: number, y: number);
5 constructor(xy: string);
6 constructor(xOrXy: number | string, y?: number) {
7 if (typeof xOrXy === "string") {
8 const [x, yy] = xOrXy.split(",").map(Number);
9 this.x = x; this.y = yy;
10 } else {
11 this.x = xOrXy; this.y = y!;
12 }
13 }
14}
📝

note

Prefer factory functions when construction variants get hard to follow.
Generic Overloads
untitled.typescript
TypeScript
1function head(s: string): string;
2function head<T>(arr: readonly T[]): T | undefined;
3function head(x: string | readonly unknown[]) {
4 return typeof x === "string" ? x[0] : x[0];
5}
6
7const a = head("abc"); // string
8const b = head([1, 2, 3]); // number | undefined
Pitfalls & Debugging
PitfallFix
Implementation not assignable to overloadsWiden implementation params to cover all cases
Wrong overload matchedReorder — most specific first
Too many overloadsSwitch to generics + conditional returns
External callers see implementation sigThey do not — only overloads are externally visible
untitled.typescript
TypeScript
1// Prefer conditional types when the mapping is purely type-level
2type Ret<T> = T extends string ? number : boolean;
3function f<T extends string | number>(x: T): Ret<T>;
4function f(x: string | number): number | boolean {
5 return typeof x === "string" ? x.length : true;
6}
Real-World: querySelector-style API
query.ts
TypeScript
1function $<E extends keyof HTMLElementTagNameMap>(
2 selectors: E,
3): HTMLElementTagNameMap[E] | null;
4function $<E extends Element = Element>(selectors: string): E | null;
5function $(selectors: string): Element | null {
6 return document.querySelector(selectors);
7}
8
9const el = $("div"); // HTMLDivElement | null
🔥

pro tip

This is the same technique the DOM lib uses — overloads keyed by literal tag names.
Additional Depth — overloads (1)

Extra worked material to reinforce overloads. Keep strict: true enabled while experimenting.

overloads-0.ts
TypeScript
1// Depth set 1 for overloads
2type Id0 = string;
3type Box0<T> = { value: T };
4function wrap0<T>(value: T): Box0<T> {
5 return { value };
6}
7const sample0 = wrap0({ ok: true as const, n: 0 });
8type Sample0 = typeof sample0;
9
10type Keys0 = keyof Sample0;
11type Val0 = Sample0["value"];
12
13type Union0 = "a" | "b" | "c";
14type Upper0 = Uppercase<Union0>;
15
16type Fn0 = (x: number) => string;
17type Ret0 = ReturnType<Fn0>;
18type Params0 = Parameters<Fn0>;
19
20type PartialUser0 = Partial<{ id: string; name: string }>;
21type RequiredUser0 = Required<PartialUser0>;
22
📝

note

If this compiles, try breaking it — remove a field, widen a literal, or add null — and read the error.
CheckPass criteria
InferenceHover types match expectations
Strict nullNo implicit undefined holes
RefactorRename propagates safely
Additional Depth — overloads (2)

Extra worked material to reinforce overloads. Keep strict: true enabled while experimenting.

overloads-1.ts
TypeScript
1// Depth set 2 for overloads
2type Id1 = string;
3type Box1<T> = { value: T };
4function wrap1<T>(value: T): Box1<T> {
5 return { value };
6}
7const sample1 = wrap1({ ok: true as const, n: 1 });
8type Sample1 = typeof sample1;
9
10type Keys1 = keyof Sample1;
11type Val1 = Sample1["value"];
12
13type Union1 = "a" | "b" | "c";
14type Upper1 = Uppercase<Union1>;
15
16type Fn1 = (x: number) => string;
17type Ret1 = ReturnType<Fn1>;
18type Params1 = Parameters<Fn1>;
19
20type PartialUser1 = Partial<{ id: string; name: string }>;
21type RequiredUser1 = Required<PartialUser1>;
22
📝

note

If this compiles, try breaking it — remove a field, widen a literal, or add null — and read the error.
CheckPass criteria
InferenceHover types match expectations
Strict nullNo implicit undefined holes
RefactorRename propagates safely
Additional Depth — overloads (3)

Extra worked material to reinforce overloads. Keep strict: true enabled while experimenting.

overloads-2.ts
TypeScript
1// Depth set 3 for overloads
2type Id2 = string;
3type Box2<T> = { value: T };
4function wrap2<T>(value: T): Box2<T> {
5 return { value };
6}
7const sample2 = wrap2({ ok: true as const, n: 2 });
8type Sample2 = typeof sample2;
9
10type Keys2 = keyof Sample2;
11type Val2 = Sample2["value"];
12
13type Union2 = "a" | "b" | "c";
14type Upper2 = Uppercase<Union2>;
15
16type Fn2 = (x: number) => string;
17type Ret2 = ReturnType<Fn2>;
18type Params2 = Parameters<Fn2>;
19
20type PartialUser2 = Partial<{ id: string; name: string }>;
21type RequiredUser2 = Required<PartialUser2>;
22
📝

note

If this compiles, try breaking it — remove a field, widen a literal, or add null — and read the error.
CheckPass criteria
InferenceHover types match expectations
Strict nullNo implicit undefined holes
RefactorRename propagates safely
Additional Depth — overloads (4)

Extra worked material to reinforce overloads. Keep strict: true enabled while experimenting.

overloads-3.ts
TypeScript
1// Depth set 4 for overloads
2type Id3 = string;
3type Box3<T> = { value: T };
4function wrap3<T>(value: T): Box3<T> {
5 return { value };
6}
7const sample3 = wrap3({ ok: true as const, n: 3 });
8type Sample3 = typeof sample3;
9
10type Keys3 = keyof Sample3;
11type Val3 = Sample3["value"];
12
13type Union3 = "a" | "b" | "c";
14type Upper3 = Uppercase<Union3>;
15
16type Fn3 = (x: number) => string;
17type Ret3 = ReturnType<Fn3>;
18type Params3 = Parameters<Fn3>;
19
20type PartialUser3 = Partial<{ id: string; name: string }>;
21type RequiredUser3 = Required<PartialUser3>;
22
📝

note

If this compiles, try breaking it — remove a field, widen a literal, or add null — and read the error.
CheckPass criteria
InferenceHover types match expectations
Strict nullNo implicit undefined holes
RefactorRename propagates safely
$Blueprint — Engineering Documentation·Section ID: TS-OVLD·Revision: 1.0

Community

Get help on Slack, Discord or VIP

Stuck on a guide? Join the community and ask.