TypeScript — Function Overloads
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.
Each overload is a separate function signature declared before the implementation. Callers see only the overload signatures, never the implementation signature.
| 1 | // Overload signatures — visible to callers |
| 2 | function format(value: string): string; |
| 3 | function format(value: number): string; |
| 4 | function format(value: Date): string; |
| 5 | |
| 6 | // Implementation signature — NOT visible to callers |
| 7 | function 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 | |
| 13 | format(" hello "); // "hello" — string overload |
| 14 | format(3.14159); // "3.14" — number overload |
| 15 | format(new Date()); // "2024-01-15" — Date overload |
The implementation signature must be compatible with all overload signatures. It is not callable directly — it only serves as the implementation body.
| 1 | // ❌ Error: implementation must be compatible with overloads |
| 2 | function bad(a: number): string; |
| 3 | function bad(a: string): number; |
| 4 | function 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 |
| 12 | function createElement(tag: "div"): HTMLDivElement; |
| 13 | function createElement(tag: "span"): HTMLSpanElement; |
| 14 | function createElement(tag: "input"): HTMLInputElement; |
| 15 | function 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) |
TypeScript resolves overloads top-to-bottom, picking the first matching signature. This means ordering matters — place more specific overloads before more general ones.
| 1 | // ✅ Correct: specific first, general last |
| 2 | function parse(input: string): object; |
| 3 | function parse(input: ArrayBuffer): object; |
| 4 | function 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 |
| 13 | function broken(input: string | ArrayBuffer): object; |
| 14 | function broken(input: string): object; // ← unreachable! |
| 15 | function 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: |
| 24 | function handle(x: string): string; |
| 25 | function handle(x: object): object; |
| 26 | function handle(x: unknown): unknown { |
| 27 | return x; |
| 28 | } |
| 29 | |
| 30 | handle("hi"); // first overload: string → string |
| 31 | handle({}); // second overload: object → object |
warning
| 1 | // Pattern 1: Varying parameter count |
| 2 | function createArray(): number[]; |
| 3 | function createArray<T>(value: T, count: number): T[]; |
| 4 | function createArray<T>(value?: T, count?: number): T[] { |
| 5 | if (value === undefined) return []; |
| 6 | return Array(count ?? 1).fill(value); |
| 7 | } |
| 8 | |
| 9 | createArray(); // number[] |
| 10 | createArray("a", 3); // string[] |
| 11 | |
| 12 | // Pattern 2: Overloaded factory |
| 13 | interface User { |
| 14 | kind: "user"; |
| 15 | name: string; |
| 16 | email: string; |
| 17 | } |
| 18 | interface Admin { |
| 19 | kind: "admin"; |
| 20 | name: string; |
| 21 | email: string; |
| 22 | permissions: string[]; |
| 23 | } |
| 24 | |
| 25 | function createUser(name: string, email: string): User; |
| 26 | function createUser( |
| 27 | name: string, |
| 28 | email: string, |
| 29 | permissions: string[] |
| 30 | ): Admin; |
| 31 | function 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 | |
| 42 | const u = createUser("Alice", "a@b.com"); // User |
| 43 | const a = createUser("Bob", "b@b.com", ["all"]); // Admin |
| 44 | |
| 45 | // Pattern 3: Type narrowing via overloads |
| 46 | function first<T>(arr: T[]): T; |
| 47 | function first<T>(arr: readonly T[]): T; |
| 48 | function first(arr: unknown[]): unknown { |
| 49 | return arr[0]; |
| 50 | } |
| 1 | // Approach 1: Overloads — explicit signatures |
| 2 | function merge(a: string, b: string): string; |
| 3 | function merge(a: number, b: number): number; |
| 4 | function 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 |
| 10 | function mergeUnion(a: string, b: string): string; |
| 11 | function mergeUnion(a: number, b: number): number; |
| 12 | function 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 |
| 18 | type 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 | |
| 28 | function 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
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.