Function overloads define multiple call signatures for a single function. Here's how to use them effectively.
Basic Syntax#
1// Overload signatures (declarations)
2function greet(name: string): string;
3function greet(name: string, age: number): string;
4
5// Implementation signature
6function greet(name: string, age?: number): string {
7 if (age !== undefined) {
8 return `Hello, ${name}! You are ${age} years old.`;
9 }
10 return `Hello, ${name}!`;
11}
12
13// Usage
14greet('Alice'); // OK
15greet('Bob', 30); // OK
16greet('Charlie', 'old'); // Error: no matching overloadDifferent Return Types#
1// Return type depends on input
2function parse(input: string): string[];
3function parse(input: number): number[];
4function parse(input: string | number): string[] | number[] {
5 if (typeof input === 'string') {
6 return input.split(',');
7 }
8 return [input];
9}
10
11const strings = parse('a,b,c'); // string[]
12const numbers = parse(42); // number[]
13
14// More complex return types
15function createElement(tag: 'div'): HTMLDivElement;
16function createElement(tag: 'span'): HTMLSpanElement;
17function createElement(tag: 'input'): HTMLInputElement;
18function createElement(tag: string): HTMLElement {
19 return document.createElement(tag);
20}
21
22const div = createElement('div'); // HTMLDivElement
23const span = createElement('span'); // HTMLSpanElement
24const input = createElement('input'); // HTMLInputElementObject Parameter Overloads#
1interface UserById {
2 id: number;
3}
4
5interface UserByEmail {
6 email: string;
7}
8
9function findUser(criteria: UserById): Promise<User>;
10function findUser(criteria: UserByEmail): Promise<User>;
11function findUser(criteria: UserById | UserByEmail): Promise<User> {
12 if ('id' in criteria) {
13 return db.findById(criteria.id);
14 }
15 return db.findByEmail(criteria.email);
16}
17
18// Usage
19findUser({ id: 123 }); // OK
20findUser({ email: 'a@b.com' }); // OK
21findUser({ id: 1, email: 'a@b.com' }); // OK (matches first overload)Optional Parameters#
1// Different signatures for optional params
2function format(value: number): string;
3function format(value: number, decimals: number): string;
4function format(value: number, decimals: number, prefix: string): string;
5function format(value: number, decimals?: number, prefix?: string): string {
6 const formatted = decimals !== undefined
7 ? value.toFixed(decimals)
8 : value.toString();
9
10 return prefix ? `${prefix}${formatted}` : formatted;
11}
12
13format(42); // '42'
14format(42.567, 2); // '42.57'
15format(42.567, 2, '$'); // '$42.57'Callback Signatures#
1// Different callback signatures
2function fetchData(url: string): Promise<unknown>;
3function fetchData(url: string, callback: (data: unknown) => void): void;
4function fetchData(
5 url: string,
6 callback?: (data: unknown) => void
7): Promise<unknown> | void {
8 const promise = fetch(url).then((r) => r.json());
9
10 if (callback) {
11 promise.then(callback);
12 return;
13 }
14
15 return promise;
16}
17
18// Promise-based
19const data = await fetchData('/api/data');
20
21// Callback-based
22fetchData('/api/data', (data) => {
23 console.log(data);
24});Generic Overloads#
1// Generic overloads
2function first<T>(arr: T[]): T | undefined;
3function first<T>(arr: T[], defaultValue: T): T;
4function first<T>(arr: T[], defaultValue?: T): T | undefined {
5 return arr.length > 0 ? arr[0] : defaultValue;
6}
7
8const num = first([1, 2, 3]); // number | undefined
9const withDefault = first([], 0); // number
10
11// Multiple generics
12function merge<T>(target: T, source: Partial<T>): T;
13function merge<T, U>(target: T, source: U): T & U;
14function merge(target: any, source: any) {
15 return { ...target, ...source };
16}
17
18const result1 = merge({ a: 1 }, { a: 2 }); // { a: number }
19const result2 = merge({ a: 1 }, { b: 'hello' }); // { a: number } & { b: string }Method Overloads#
1class Calculator {
2 // Method overloads
3 add(a: number, b: number): number;
4 add(a: string, b: string): string;
5 add(a: number | string, b: number | string): number | string {
6 if (typeof a === 'number' && typeof b === 'number') {
7 return a + b;
8 }
9 return String(a) + String(b);
10 }
11}
12
13const calc = new Calculator();
14const sum = calc.add(1, 2); // number
15const concat = calc.add('a', 'b'); // stringEvent Handler Overloads#
1interface ClickEvent {
2 type: 'click';
3 x: number;
4 y: number;
5}
6
7interface KeyEvent {
8 type: 'keydown';
9 key: string;
10}
11
12function handleEvent(event: ClickEvent): void;
13function handleEvent(event: KeyEvent): void;
14function handleEvent(event: ClickEvent | KeyEvent): void {
15 if (event.type === 'click') {
16 console.log(`Click at ${event.x}, ${event.y}`);
17 } else {
18 console.log(`Key pressed: ${event.key}`);
19 }
20}
21
22// Type-safe event handling
23handleEvent({ type: 'click', x: 100, y: 200 });
24handleEvent({ type: 'keydown', key: 'Enter' });API Response Types#
1interface User {
2 id: number;
3 name: string;
4}
5
6interface Post {
7 id: number;
8 title: string;
9}
10
11function fetch(endpoint: '/users'): Promise<User[]>;
12function fetch(endpoint: '/users', id: number): Promise<User>;
13function fetch(endpoint: '/posts'): Promise<Post[]>;
14function fetch(endpoint: '/posts', id: number): Promise<Post>;
15function fetch(endpoint: string, id?: number): Promise<unknown> {
16 const url = id ? `${endpoint}/${id}` : endpoint;
17 return globalThis.fetch(url).then((r) => r.json());
18}
19
20const users = await fetch('/users'); // User[]
21const user = await fetch('/users', 1); // User
22const posts = await fetch('/posts'); // Post[]
23const post = await fetch('/posts', 1); // PostConstructor Overloads#
1class DateRange {
2 start: Date;
3 end: Date;
4
5 // Constructor overloads
6 constructor(start: Date, end: Date);
7 constructor(range: { start: Date; end: Date });
8 constructor(startOrRange: Date | { start: Date; end: Date }, end?: Date) {
9 if (startOrRange instanceof Date) {
10 this.start = startOrRange;
11 this.end = end!;
12 } else {
13 this.start = startOrRange.start;
14 this.end = startOrRange.end;
15 }
16 }
17}
18
19new DateRange(new Date(), new Date());
20new DateRange({ start: new Date(), end: new Date() });Interface Method Overloads#
1interface StringTransformer {
2 (input: string): string;
3 (input: string[]): string[];
4}
5
6const uppercase: StringTransformer = (input: string | string[]) => {
7 if (Array.isArray(input)) {
8 return input.map((s) => s.toUpperCase());
9 }
10 return input.toUpperCase();
11};
12
13const single = uppercase('hello'); // string
14const multiple = uppercase(['a', 'b']); // string[]Narrowing with Overloads#
1// Use overloads for better narrowing
2function getValue(key: 'name'): string;
3function getValue(key: 'age'): number;
4function getValue(key: 'active'): boolean;
5function getValue(key: string): unknown {
6 const values = {
7 name: 'Alice',
8 age: 30,
9 active: true,
10 };
11 return values[key as keyof typeof values];
12}
13
14const name = getValue('name'); // string
15const age = getValue('age'); // number
16const active = getValue('active'); // booleanBest Practices#
Design:
✓ Order overloads specific to general
✓ Keep implementation signature broad
✓ Document each overload
✓ Use for distinct behaviors
Patterns:
✓ Different return types
✓ Optional callback vs Promise
✓ Type narrowing by parameter
✓ Builder pattern APIs
Avoid:
✗ Too many overloads (> 4-5)
✗ Overloads for default values
✗ Complex conditional returns
✗ Overloads that could be unions
Alternatives:
✓ Union types when simpler
✓ Generics for type inference
✓ Conditional types for complex cases
✓ Separate functions for clarity
Conclusion#
Function overloads provide multiple type-safe ways to call a function. Use them when the return type depends on input types, or when different parameter combinations have distinct behaviors. Order overloads from most specific to least specific, and keep the implementation signature compatible with all overloads.