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

$Blueprint — Engineering Documentation·Section ID: TS-OVLD·Revision: 1.0