Function overloading lets you define multiple type signatures for the same function. This allows a function to behave differently based on the number or types of arguments passed, while maintaining complete type safety. TypeScript's overloading is purely a compile-time feature that provides better type checking and autocomplete. Let's master function overloading!
What Is Function Overloading?
Function overloading allows you to define multiple signatures for a single function. Each signature describes a different way to call the function, and TypeScript enforces the correct usage.
// Overload signatures (declarations)
function greet(name: string): string;
function greet(firstName: string, lastName: string): string;
// Implementation signature (must handle all overloads)
function greet(firstName: string, lastName?: string): string {
if (lastName) {
return `Hello, ${firstName} ${lastName}!`;
}
return `Hello, ${firstName}!`;
}
// Usage - TypeScript knows which overload matches
const greeting1 = greet("Yusuf"); // Uses first overload
const greeting2 = greet("Yusuf", "Ibrahim"); // Uses second overload
console.log(greeting1); // "Hello, Yusuf!"
console.log(greeting2); // "Hello, Yusuf Ibrahim!"
// These would cause errors:
// greet(); // ❌ Error: Expected 1-2 arguments
// greet("A", "B", "C"); // ❌ Error: Expected 1-2 arguments
// const x: number = greet("Yusuf"); // ❌ Error: Type 'string' not assignable to 'number'Overload Structure
// 1. Overload signatures (visible to callers)
function myFunction(param: Type1): ReturnType1;
function myFunction(param: Type2): ReturnType2;
function myFunction(param1: Type3, param2: Type4): ReturnType3;
// 2. Implementation signature (not visible to callers)
function myFunction(param1: Type1 | Type2 | Type3, param2?: Type4): ReturnType1 | ReturnType2 | ReturnType3 {
// Implementation that handles all cases
}Important: Only the overload signatures are visible to users of your function. The implementation signature is internal and must be broad enough to handle all overload cases.
Basic Overloading Patterns
Different Number of Parameters
// Overload by number of parameters
function createDate(): Date;
function createDate(timestamp: number): Date;
function createDate(year: number, month: number, day: number): Date;
function createDate(yearOrTimestamp?: number, month?: number, day?: number): Date {
if (yearOrTimestamp === undefined) {
return new Date();
}
if (month === undefined || day === undefined) {
// Single number provided - treat as timestamp
return new Date(yearOrTimestamp);
}
// Three numbers provided - treat as year, month, day
return new Date(yearOrTimestamp, month - 1, day);
}
const now = createDate(); // Current date
const fromTimestamp = createDate(1640000000000); // From timestamp
const specific = createDate(2024, 12, 25); // Specific date
console.log(now);
console.log(fromTimestamp);
console.log(specific);Different Parameter Types
// Overload by parameter type
function formatValue(value: string): string;
function formatValue(value: number): string;
function formatValue(value: boolean): string;
function formatValue(value: string | number | boolean): string {
if (typeof value === "string") {
return `"${value}"`;
}
if (typeof value === "number") {
return value.toFixed(2);
}
return value ? "Yes" : "No";
}
console.log(formatValue("Hello")); // ""Hello""
console.log(formatValue(123.456)); // "123.46"
console.log(formatValue(true)); // "Yes"
console.log(formatValue(false)); // "No"
// TypeScript knows the return type is always string
const result: string = formatValue(42);Different Return Types
// Overload with different return types based on input
function getValue(id: number): string;
function getValue(name: string): number;
function getValue(idOrName: number | string): string | number {
if (typeof idOrName === "number") {
return `User-${idOrName}`; // Returns string for number input
}
return idOrName.length; // Returns number for string input
}
const str = getValue(123); // Type: string
const num = getValue("Yusuf"); // Type: number
console.log(str); // "User-123"
console.log(num); // 5
// TypeScript enforces correct types
// const wrong: number = getValue(123); // ❌ Error: Type 'string' not assignable to 'number'Advanced Overloading Patterns
Generic Overloads
// Overload with generics
function map<T, U>(array: T[], fn: (item: T) => U): U[];
function map<T, U>(array: T[], fn: (item: T, index: number) => U): U[];
function map<T, U>(
array: T[],
fn: (item: T, index?: number) => U
): U[] {
return array.map(fn);
}
const numbers = [1, 2, 3, 4, 5];
// First overload - function with one parameter
const doubled = map(numbers, (n) => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
// Second overload - function with two parameters
const indexed = map(numbers, (n, i) => `${i}: ${n}`);
console.log(indexed); // ["0: 1", "1: 2", "2: 3", "3: 4", "4: 5"]
// Generic array transformation
const users = [
{ name: "Yusuf", age: 25 },
{ name: "Amina", age: 30 }
];
const names = map(users, (user) => user.name);
console.log(names); // ["Yusuf", "Amina"]Conditional Return Types
// Return type depends on parameter type
function parseValue(value: string, asNumber: true): number;
function parseValue(value: string, asNumber: false): string;
function parseValue(value: string, asNumber?: boolean): string | number;
function parseValue(
value: string,
asNumber: boolean = false
): string | number {
if (asNumber) {
return parseFloat(value);
}
return value;
}
const num = parseValue("123.45", true); // Type: number
const str = parseValue("123.45", false); // Type: string
const either = parseValue("123.45"); // Type: string | number
console.log(typeof num); // "number"
console.log(typeof str); // "string"
console.log(typeof either); // "string"
// Usage with API response
function fetchData(url: string, parse: true): Promise<object>;
function fetchData(url: string, parse: false): Promise<string>;
function fetchData(url: string, parse?: boolean): Promise<object | string>;
async function fetchData(
url: string,
parse: boolean = true
): Promise<object | string> {
const response = await fetch(url);
const text = await response.text();
if (parse) {
return JSON.parse(text);
}
return text;
}
// TypeScript knows the return type
const jsonData = await fetchData("/api/users", true); // Type: object
const rawData = await fetchData("/api/users", false); // Type: stringComplex Overloading
// Complex overload with multiple parameter combinations
function createElement(tag: "div"): HTMLDivElement;
function createElement(tag: "span"): HTMLSpanElement;
function createElement(tag: "input"): HTMLInputElement;
function createElement(tag: string): HTMLElement;
function createElement(tag: string): HTMLElement {
return document.createElement(tag);
}
// TypeScript infers specific element types
const div = createElement("div"); // Type: HTMLDivElement
const span = createElement("span"); // Type: HTMLSpanElement
const input = createElement("input"); // Type: HTMLInputElement
const custom = createElement("custom"); // Type: HTMLElement
// Can use specific methods
div.style.display = "block";
input.value = "text";
// Database query overloads
function query(sql: string): Promise<unknown[]>;
function query(sql: string, params: any[]): Promise<unknown[]>;
function query<T>(sql: string, params: any[], transform: (row: any) => T): Promise<T[]>;
async function query<T>(
sql: string,
params?: any[],
transform?: (row: any) => T
): Promise<T[] | unknown[]> {
console.log(`Executing: ${sql}`);
console.log(`Parameters:`, params);
// Simulate query execution
const rows = [{ id: 1, name: "Yusuf" }, { id: 2, name: "Amina" }];
if (transform) {
return rows.map(transform);
}
return rows;
}
// Different ways to use the query function
const results1 = await query("SELECT * FROM users");
const results2 = await query("SELECT * FROM users WHERE id = ?", [1]);
const results3 = await query(
"SELECT * FROM users",
[],
(row) => ({ userId: row.id, userName: row.name })
);Method Overloading in Classes
Overloading works with class methods too, following the same pattern.
class Calculator {
// Overload signatures
add(a: number, b: number): number;
add(a: string, b: string): string;
// Implementation
add(a: number | string, b: number | string): number | string {
if (typeof a === "number" && typeof b === "number") {
return a + b;
}
return String(a) + String(b);
}
// Overload for multiplication
multiply(a: number, b: number): number;
multiply(a: number, b: number, c: number): number;
multiply(a: number, b: number, c?: number): number {
if (c !== undefined) {
return a * b * c;
}
return a * b;
}
}
const calc = new Calculator();
console.log(calc.add(5, 10)); // 15 (number)
console.log(calc.add("Hello", " World")); // "Hello World" (string)
console.log(calc.multiply(2, 3)); // 6
console.log(calc.multiply(2, 3, 4)); // 24
// Array utility class
class ArrayUtils {
// Get element by index or default
get<T>(array: T[], index: number): T | undefined;
get<T>(array: T[], index: number, defaultValue: T): T;
get<T>(array: T[], index: number, defaultValue?: T): T | undefined {
if (index >= 0 && index < array.length) {
return array[index];
}
return defaultValue;
}
// Filter array
filter<T>(array: T[], predicate: (item: T) => boolean): T[];
filter<T>(array: T[], predicate: (item: T, index: number) => boolean): T[];
filter<T>(
array: T[],
predicate: (item: T, index?: number) => boolean
): T[] {
return array.filter(predicate);
}
}
const utils = new ArrayUtils();
const numbers = [1, 2, 3, 4, 5];
console.log(utils.get(numbers, 2)); // 3
console.log(utils.get(numbers, 10)); // undefined
console.log(utils.get(numbers, 10, 0)); // 0 (default)
const evens = utils.filter(numbers, (n) => n % 2 === 0);
console.log(evens); // [2, 4]Practical Examples
Example 1: Data Fetcher
// Flexible fetch function with overloads
type User = { id: number; name: string; email: string };
type Product = { id: number; name: string; price: number };
// Fetch single item by ID
function fetch(resource: "user", id: number): Promise<User>;
function fetch(resource: "product", id: number): Promise<Product>;
// Fetch all items
function fetch(resource: "users"): Promise<User[]>;
function fetch(resource: "products"): Promise<Product[]>;
// Implementation
async function fetch(
resource: "user" | "product" | "users" | "products",
id?: number
): Promise<User | Product | User[] | Product[]> {
console.log(`Fetching ${resource}${id ? ` with ID ${id}` : ""}`);
// Simulate API calls
if (resource === "user" && id) {
return { id, name: "Yusuf Ibrahim", email: "yusuf@example.com" };
}
if (resource === "product" && id) {
return { id, name: "Laptop", price: 450000 };
}
if (resource === "users") {
return [
{ id: 1, name: "Yusuf", email: "yusuf@example.com" },
{ id: 2, name: "Amina", email: "amina@example.com" }
];
}
if (resource === "products") {
return [
{ id: 1, name: "Laptop", price: 450000 },
{ id: 2, name: "Mouse", price: 5000 }
];
}
throw new Error(`Unknown resource: ${resource}`);
}
// Usage - TypeScript knows exact return types
const user = await fetch("user", 1); // Type: User
const product = await fetch("product", 2); // Type: Product
const users = await fetch("users"); // Type: User[]
const products = await fetch("products"); // Type: Product[]
console.log(user.name); // TypeScript knows this is User
console.log(product.price); // TypeScript knows this is ProductExample 2: Event Emitter
type EventMap = {
"user:login": { userId: number; timestamp: Date };
"user:logout": { userId: number; timestamp: Date };
"order:created": { orderId: number; amount: number };
"order:completed": { orderId: number };
};
class EventEmitter {
private handlers: Map<string, Function[]> = new Map();
// Overload for different event types
on<K extends keyof EventMap>(
event: K,
handler: (data: EventMap[K]) => void
): void;
on(event: string, handler: Function): void {
if (!this.handlers.has(event)) {
this.handlers.set(event, []);
}
this.handlers.get(event)!.push(handler);
}
// Overload for emitting events
emit<K extends keyof EventMap>(event: K, data: EventMap[K]): void;
emit(event: string, data: any): void {
const handlers = this.handlers.get(event);
if (handlers) {
handlers.forEach(handler => handler(data));
}
}
}
const emitter = new EventEmitter();
// TypeScript enforces correct data types
emitter.on("user:login", (data) => {
// data is typed as { userId: number; timestamp: Date }
console.log(`User ${data.userId} logged in at ${data.timestamp}`);
});
emitter.on("order:created", (data) => {
// data is typed as { orderId: number; amount: number }
console.log(`Order ${data.orderId} created for ₦${data.amount}`);
});
// Emit events with type safety
emitter.emit("user:login", {
userId: 1,
timestamp: new Date()
});
emitter.emit("order:created", {
orderId: 1001,
amount: 50000
});
// These would cause errors:
// emitter.emit("user:login", { userId: "1" }); // ❌ Wrong type
// emitter.emit("order:created", { orderId: 1 }); // ❌ Missing amountExample 3: Form Validator
type ValidationResult = { valid: true } | { valid: false; error: string };
class Validator {
// Validate email
validate(type: "email", value: string): ValidationResult;
// Validate age
validate(type: "age", value: number): ValidationResult;
// Validate password
validate(
type: "password",
value: string,
minLength: number
): ValidationResult;
// Implementation
validate(
type: "email" | "age" | "password",
value: string | number,
minLength?: number
): ValidationResult {
if (type === "email" && typeof value === "string") {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (emailRegex.test(value)) {
return { valid: true };
}
return { valid: false, error: "Invalid email format" };
}
if (type === "age" && typeof value === "number") {
if (value >= 18 && value <= 120) {
return { valid: true };
}
return { valid: false, error: "Age must be between 18 and 120" };
}
if (type === "password" && typeof value === "string") {
const min = minLength ?? 8;
if (value.length >= min) {
return { valid: true };
}
return {
valid: false,
error: `Password must be at least ${min} characters`
};
}
return { valid: false, error: "Unknown validation type" };
}
}
const validator = new Validator();
// Email validation
const email1 = validator.validate("email", "yusuf@example.com");
console.log(email1); // { valid: true }
const email2 = validator.validate("email", "invalid-email");
console.log(email2); // { valid: false, error: "Invalid email format" }
// Age validation
const age1 = validator.validate("age", 25);
console.log(age1); // { valid: true }
const age2 = validator.validate("age", 15);
console.log(age2); // { valid: false, error: "Age must be between 18 and 120" }
// Password validation
const pwd1 = validator.validate("password", "secret123");
console.log(pwd1); // { valid: true }
const pwd2 = validator.validate("password", "short", 10);
console.log(pwd2); // { valid: false, error: "Password must be at least 10 characters" }Test Your Knowledge
Type Safety Check
Which function overload structure is correct?
// Option A
function format(value: string): string {
return value.toUpperCase();
}
function format(value: number): string {
return value.toFixed(2);
}
// Option B
function format(value: string): string;
function format(value: number): string;
function format(value: string | number): string {
if (typeof value === "string") return value.toUpperCase();
return value.toFixed(2);
}
// Option C
function format(value: string | number): string {
if (typeof value === "string") return value.toUpperCase();
return value.toFixed(2);
}
// Option D
function format(value: string): string;
function format(value: number): string;Common TypeScript Error
// Incorrect overload - implementation not compatible
function getValue(id: number): string;
function getValue(name: string): number;
function getValue(value: number | string): string { // Error!
if (typeof value === "number") {
return "ID-" + value; // Returns string ✓
}
return value.length; // Returns number ✗ (should return string)
}❌ This signature is incompatible with the implementation signature. Type 'number' is not assignable to type 'string'.
What's Wrong?
The implementation signature must be compatible with all overload signatures. Here, the second overload says it returns a number, but the implementation signature says it returns a string.
// Fixed: Implementation compatible with all overloads
function getValue(id: number): string;
function getValue(name: string): number;
function getValue(value: number | string): string | number {
if (typeof value === "number") {
return "ID-" + value; // Returns string
}
return value.length; // Returns number
}
const result1 = getValue(123); // Type: string
const result2 = getValue("test"); // Type: numberSolution: Make sure the implementation signature is broad enough to cover all overloads
Overloading vs Union Types
Sometimes you can achieve similar results with union types instead of overloading. Here's when to use each:
// Option 1: Using overloads (better type inference)
function processOverload(value: string): string[];
function processOverload(value: number): number[];
function processOverload(value: string | number): string[] | number[] {
if (typeof value === "string") {
return value.split("");
}
return [value, value * 2, value * 3];
}
const result1 = processOverload("hello"); // Type: string[]
const result2 = processOverload(5); // Type: number[]
// TypeScript knows exact return type for each call
result1.forEach(char => console.log(char.toUpperCase())); // ✅ Works
result2.forEach(num => console.log(num.toFixed(2))); // ✅ Works
// Option 2: Using union types (simpler but less precise)
function processUnion(value: string | number): string[] | number[] {
if (typeof value === "string") {
return value.split("");
}
return [value, value * 2, value * 3];
}
const result3 = processUnion("hello"); // Type: string[] | number[]
const result4 = processUnion(5); // Type: string[] | number[]
// TypeScript doesn't know which type, so these need type guards
if (typeof result3[0] === "string") {
result3.forEach(char => console.log(char.toUpperCase()));
}💡 When to Use Overloading vs Unions
- Use Overloading: When return type depends on input type/parameters
- Use Overloading: When you want precise type inference for callers
- Use Overloading: For better autocomplete and IDE support
- Use Union Types: When implementation is simple and return type is consistent
- Use Union Types: When overloading would require too many signatures
Best Practices
- Order matters: Put most specific overloads first, general ones last
- Implementation must cover all cases: Make sure it handles every overload
- Keep overloads related: They should represent different ways to call the same logical function
- Don't overuse: Too many overloads make functions hard to understand
- Document overloads: Use JSDoc to explain each signature
- Consider alternatives: Sometimes generics or union types are clearer
- Test all paths: Ensure implementation works for every overload
- Use for clarity: Overloading should make the API easier to use, not harder
// ✅ Good: Clear, related overloads
/**
* Formats a date value
* @param date - Date object to format
* @returns Formatted date string
*/
function formatDate(date: Date): string;
/**
* Formats a timestamp
* @param timestamp - Unix timestamp in milliseconds
* @returns Formatted date string
*/
function formatDate(timestamp: number): string;
function formatDate(dateOrTimestamp: Date | number): string {
const date = typeof dateOrTimestamp === "number"
? new Date(dateOrTimestamp)
: dateOrTimestamp;
return date.toLocaleDateString();
}
// ✅ Good: Specific before general
function getValue(id: number): string;
function getValue(name: string): number;
function getValue(value: number | string): string | number {
// Implementation
return typeof value === "number" ? `ID-${value}` : value.length;
}
// ❌ Avoid: Too many unrelated overloads
function badFunction(a: string): string;
function badFunction(a: number): number;
function badFunction(a: boolean): boolean;
function badFunction(a: string[]): string[];
function badFunction(a: number[]): number[];
// ... 10 more overloads
// This is confusing and hard to maintain
// ✅ Better: Use generics or union types instead
function betterFunction<T>(value: T): T {
return value;
}
// ❌ Avoid: Implementation not compatible
function wrong(x: number): string;
function wrong(x: string): number;
function wrong(x: number | string): number { // ❌ Error!
// This doesn't return string for number input
return typeof x === "number" ? x : x.length;
}
// ✅ Good: Compatible implementation
function correct(x: number): string;
function correct(x: string): number;
function correct(x: number | string): string | number {
return typeof x === "number" ? `${x}` : x.length;
}Key Takeaways
- Function overloading defines multiple signatures for one function
- Overload signatures come first, implementation signature last
- Only overload signatures are visible to callers
- Implementation must be compatible with all overload signatures
- Overloading provides precise type inference based on arguments
- Order overloads from most specific to most general
- Overloading works with functions, methods, and constructors
- Use overloading when return type depends on input type
- Consider union types or generics for simpler cases
- Overloading is a compile-time feature for better type safety
What's Next?
Excellent work! You've mastered function overloading and can now create sophisticated, type-safe APIs. This completes the "Functions & Interfaces" category! Next, we'll move into Advanced Types starting with Union and Intersection Types. You'll learn how to combine types in powerful ways, create flexible type definitions, and handle complex type scenarios!
Get ready to unlock TypeScript's advanced type system!