Falytom logoFALYTOM
Games
ToolsUI ComponentsServicesProductsAbout
LoginSign Up
menu
  1. Home
  2. /Web Development
  3. /Typescript
  4. /Function Overloading
Your Progress0%
0 of 30 completed

TypeScript Topics

Getting Started

  • What is TypeScript?
  • TypeScript vs JavaScript
  • Setting Up TypeScript
  • Your First TypeScript Program

Basic Types

  • Primitive Types
  • Arrays and Tuples
  • Objects and Type Aliases
  • Enums
  • Any, Unknown, and Never

Functions & Interfaces

  • Function Types and Signatures
  • Interfaces
  • Optional and Default Parameters
  • Function Overloading

Advanced Types

  • Union and Intersection Types
  • Type Guards and Narrowing
  • Literal Types
  • Type Assertions and Casting

Generics

  • Introduction to Generics
  • Generic Constraints
  • Utility Types

Classes & OOP

  • Classes and Constructors
  • Access Modifiers
  • Inheritance and Polymorphism
  • Abstract Classes and Interfaces

Practical TypeScript

  • Working with Modules
  • TypeScript with React
  • Async/Await and Promises
  • Working with APIs and JSON
  • Type Declarations and DefinitelyTyped
  • TypeScript Best Practices

Function Overloading

Multiple signatures for the same function

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.

overloading-basics.ts
// 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

overload-structure.ts
// 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

different-parameter-counts.ts
// 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

different-parameter-types.ts
// 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

different-return-types.ts
// 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

generic-overloads.ts
// 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

conditional-returns.ts
// 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: string

Complex Overloading

complex-overloading.ts
// 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-overloading.ts
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

data-fetcher.ts
// 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 Product

Example 2: Event Emitter

event-emitter.ts
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 amount

Example 3: Form Validator

form-validator.ts
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

Code with 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.

Corrected Code
// 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: number

Solution: 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:

overloading-vs-unions.ts
// 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

  1. Order matters: Put most specific overloads first, general ones last
  2. Implementation must cover all cases: Make sure it handles every overload
  3. Keep overloads related: They should represent different ways to call the same logical function
  4. Don't overuse: Too many overloads make functions hard to understand
  5. Document overloads: Use JSDoc to explain each signature
  6. Consider alternatives: Sometimes generics or union types are clearer
  7. Test all paths: Ensure implementation works for every overload
  8. Use for clarity: Overloading should make the API easier to use, not harder
best-practices-example.ts
// ✅ 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!

Mastering TypeScript function overloading! Check this out!

Previous
Optional and Default Parameters
Next
Union and Intersection Types

Never Miss a New TypeScript Tutorial

Join 1,000+ developers learning TypeScript step-by-step. Get new tutorials, type safety tips, and exclusive resources delivered to your inbox - completely FREE.

You might also like

UI Components

Ready-made components for your projects

Developer Tools

Boost productivity with handy generators

HTML Game

Learn HTML concepts through interactive gameplay

Join Our Web Development Training Program

Master frontend & backend development with hands-on projects.

TypeScript Tutorials

0 of 30 completed

Your Progress0%

Topics

Getting Started

  • What is TypeScript?
  • TypeScript vs JavaScript
  • Setting Up TypeScript
  • Your First TypeScript Program

Basic Types

  • Primitive Types
  • Arrays and Tuples
  • Objects and Type Aliases
  • Enums
  • Any, Unknown, and Never

Functions & Interfaces

  • Function Types and Signatures
  • Interfaces
  • Optional and Default Parameters
  • Function Overloading

Advanced Types

  • Union and Intersection Types
  • Type Guards and Narrowing
  • Literal Types
  • Type Assertions and Casting

Generics

  • Introduction to Generics
  • Generic Constraints
  • Utility Types

Classes & OOP

  • Classes and Constructors
  • Access Modifiers
  • Inheritance and Polymorphism
  • Abstract Classes and Interfaces

Practical TypeScript

  • Working with Modules
  • TypeScript with React
  • Async/Await and Promises
  • Working with APIs and JSON
  • Type Declarations and DefinitelyTyped
  • TypeScript Best Practices
Falytom logoFALYTOM

Learn technologies like HTML, CSS, JavaScript, React, NodeJS and more through interactive games, tutorials, components and free tools. Transform your businesses with AI chatbots, SEO-optimized websites, and professional development services.

© 2025 Tomilola Group
  • Facebook logo
  • Twitter logo
  • Instagram logo
  • LinkedIn logo
  • GitHub logo
  • YouTube logo