+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 18 of 355

๐ŸŽฏ Function Overloading: Multiple Function Signatures in TypeScript

Master function overloading in TypeScript to create flexible APIs with multiple signatures and crystal-clear type safety ๐Ÿš€

๐Ÿš€Intermediate
30 min read

Prerequisites

  • Strong TypeScript function knowledge ๐Ÿ“
  • Understanding of union types โšก
  • Familiarity with type guards ๐Ÿ’ป

What you'll learn

  • Create functions with multiple signatures ๐ŸŽฏ
  • Implement proper overload ordering ๐Ÿ—๏ธ
  • Build flexible and type-safe APIs ๐Ÿ”
  • Master real-world overloading patterns โœจ

๐ŸŽฏ Introduction

Welcome to the powerful world of function overloading in TypeScript! ๐ŸŽ‰ In this guide, weโ€™ll explore how to create functions that adapt to different argument types while maintaining rock-solid type safety.

Imagine a Swiss Army knife ๐Ÿงฝ - one tool, multiple functions, each perfectly suited for its task. Thatโ€™s function overloading! It lets you define multiple ways to call the same function, each with its own signature and behavior.

By the end of this tutorial, youโ€™ll be creating elegant APIs that feel magical to use but are crystal clear to TypeScript! Letโ€™s dive in! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding Function Overloading

๐Ÿค” What is Function Overloading?

Function overloading is like having a restaurant menu ๐Ÿด with combo meals - same restaurant, different options, each perfectly crafted for different appetites!

In TypeScript, it allows you to:

  • โœจ Define multiple function signatures for the same function
  • ๐Ÿš€ Provide different parameter types and return types
  • ๐Ÿ›ก๏ธ Guide TypeScript to understand which version youโ€™re calling
  • ๐ŸŽจ Create flexible APIs that feel natural to use

๐Ÿ’ก Why Use Function Overloading?

Hereโ€™s why function overloading is a game-changer:

  1. Flexible APIs ๐ŸŒˆ: One function, multiple ways to use it
  2. Type Precision ๐ŸŽฏ: Each call gets exact type checking
  3. Better IntelliSense ๐Ÿ’ป: IDE shows all available signatures
  4. Self-Documenting ๐Ÿ“–: Signatures show all usage patterns
  5. Backward Compatibility ๐Ÿ”„: Add new signatures without breaking code

Real-world example: Think of a createElement function ๐Ÿ—๏ธ that can create different HTML elements based on the tag name - each tag gets its own specific return type!

๐Ÿ”ง Basic Syntax and Usage

๐Ÿ“ Function Overloading Basics

Letโ€™s start with the fundamental syntax:

// ๐ŸŽจ Step 1: Define overload signatures (what users see)
function greet(name: string): string;
function greet(name: string, age: number): string;
function greet(name: string, options: { formal: boolean }): string;

// ๐Ÿ—๏ธ Step 2: Implementation signature (not visible to users)
function greet(
  name: string,
  ageOrOptions?: number | { formal: boolean }
): string {
  if (typeof ageOrOptions === "number") {
    return `Hello ${name}, you are ${ageOrOptions} years old! ๐ŸŽ‚`;
  } else if (ageOrOptions && typeof ageOrOptions === "object") {
    return ageOrOptions.formal
      ? `Good day, ${name}. ๐ŸŽฉ`
      : `Hey ${name}! ๐Ÿ‘‹`;
  }
  return `Hello ${name}! ๐Ÿ˜Š`;
}

// ๐ŸŽฏ Usage - TypeScript knows exactly which overload you're using!
console.log(greet("Alice"));                    // "Hello Alice! ๐Ÿ˜Š"
console.log(greet("Bob", 25));                  // "Hello Bob, you are 25 years old! ๐ŸŽ‚"
console.log(greet("Charlie", { formal: true })); // "Good day, Charlie. ๐ŸŽฉ"

๐ŸŽฏ Key Concepts

// ๐Ÿ“š Overload signatures must be MORE SPECIFIC than implementation

// โœ… Good - specific overloads, general implementation
function processData(data: string): string;
function processData(data: number): number;
function processData(data: string[]): string[];
function processData(data: string | number | string[]): string | number | string[] {
  if (typeof data === "string") {
    return data.toUpperCase();
  } else if (typeof data === "number") {
    return data * 2;
  } else {
    return data.map(s => s.toUpperCase());
  }
}

// โŒ Bad - implementation too specific
// function processData(data: string): string;
// function processData(data: string): string { } // Error!

// ๐ŸŽจ Order matters! Most specific first
function parseInput(input: "true" | "false"): boolean;
function parseInput(input: string): string;
function parseInput(input: number): number;
function parseInput(input: string | number): string | number | boolean {
  if (input === "true") return true;
  if (input === "false") return false;
  return input;
}

// TypeScript picks the first matching overload
const bool = parseInput("true");  // boolean โœ…
const str = parseInput("hello");  // string โœ…
const num = parseInput(42);       // number โœ…

๐Ÿ” Understanding the Pattern

// ๐Ÿ—๏ธ The pattern:
// 1. Overload signatures (what users see)
// 2. Implementation signature (hidden from users)
// 3. Implementation body (handles all cases)

interface Coordinate {
  x: number;
  y: number;
}

// ๐ŸŽฏ Overloads for different input formats
function createPoint(): Coordinate;
function createPoint(x: number, y: number): Coordinate;
function createPoint(coord: Coordinate): Coordinate;
function createPoint(coords: [number, number]): Coordinate;

// ๐Ÿ”ง Implementation handles all cases
function createPoint(
  xOrCoord?: number | Coordinate | [number, number],
  y?: number
): Coordinate {
  if (xOrCoord === undefined) {
    return { x: 0, y: 0 }; // Default origin
  } else if (typeof xOrCoord === "number" && typeof y === "number") {
    return { x: xOrCoord, y }; // Two numbers
  } else if (Array.isArray(xOrCoord)) {
    return { x: xOrCoord[0], y: xOrCoord[1] }; // Array
  } else {
    return xOrCoord as Coordinate; // Object
  }
}

// ๐ŸŽฎ All these work with proper types!
const origin = createPoint();              // { x: 0, y: 0 }
const point1 = createPoint(10, 20);       // { x: 10, y: 20 }
const point2 = createPoint({ x: 5, y: 15 }); // { x: 5, y: 15 }
const point3 = createPoint([30, 40]);     // { x: 30, y: 40 }

๐Ÿ’ก Practical Examples

๐Ÿ›’ Example 1: Flexible Data Formatter

Letโ€™s build a versatile formatting function:

// ๐ŸŽจ Format different types of data with appropriate return types
function format(value: number): string;
function format(value: Date): string;
function format(value: boolean): "Yes" | "No";
function format<T>(value: T[]): string[];
function format(value: Record<string, any>): string;

function format(
  value: number | Date | boolean | any[] | Record<string, any>
): string | "Yes" | "No" | string[] {
  if (typeof value === "number") {
    return `$${value.toFixed(2)} ๐Ÿ’ฐ`;
  } else if (value instanceof Date) {
    return `๐Ÿ“… ${value.toLocaleDateString()}`;
  } else if (typeof value === "boolean") {
    return value ? "Yes" : "No";
  } else if (Array.isArray(value)) {
    return value.map(v => String(v));
  } else {
    return `{${Object.keys(value).join(", ")}} ๐Ÿ—บ๏ธ`;
  }
}

// ๐ŸŽฏ TypeScript knows the exact return type for each call!
const price = format(99.99);              // string: "$99.99 ๐Ÿ’ฐ"
const date = format(new Date());          // string: "๐Ÿ“… 6/18/2025"
const bool = format(true);                // "Yes" (literal type!)
const arr = format([1, 2, 3]);            // string[]
const obj = format({ x: 10, y: 20 });     // string: "{x, y} ๐Ÿ—บ๏ธ"

console.log(price);
console.log(date);
console.log(bool);
console.log(arr);
console.log(obj);

๐ŸŽจ Example 2: Smart Query Builder

Letโ€™s create a database query builder with overloads:

// ๐Ÿ” Different query types with specific return types
interface User {
  id: number;
  name: string;
  email: string;
  role: "admin" | "user";
}

interface QueryOptions {
  limit?: number;
  offset?: number;
  orderBy?: string;
}

class Database {
  // ๐ŸŽฏ Overloads for different query patterns
  find(id: number): User | undefined;
  find(email: string): User | undefined;
  find(query: { role: "admin" | "user" }): User[];
  find(query: QueryOptions): User[];
  find(): User[];
  
  find(
    idOrQuery?: number | string | { role: "admin" | "user" } | QueryOptions
  ): User | User[] | undefined {
    // Mock data
    const users: User[] = [
      { id: 1, name: "Alice", email: "[email protected]", role: "admin" },
      { id: 2, name: "Bob", email: "[email protected]", role: "user" },
      { id: 3, name: "Charlie", email: "[email protected]", role: "user" }
    ];
    
    if (typeof idOrQuery === "number") {
      console.log(`๐ŸŽฏ Finding user by ID: ${idOrQuery}`);
      return users.find(u => u.id === idOrQuery);
    } else if (typeof idOrQuery === "string") {
      console.log(`๐Ÿ“ง Finding user by email: ${idOrQuery}`);
      return users.find(u => u.email === idOrQuery);
    } else if (idOrQuery && "role" in idOrQuery) {
      console.log(`๐Ÿ‘ฅ Finding users by role: ${idOrQuery.role}`);
      return users.filter(u => u.role === idOrQuery.role);
    } else if (idOrQuery && ("limit" in idOrQuery || "offset" in idOrQuery)) {
      console.log(`๐Ÿ“‹ Query with options:`, idOrQuery);
      let result = [...users];
      if (idOrQuery.orderBy) {
        result.sort((a, b) => a.name.localeCompare(b.name));
      }
      const start = idOrQuery.offset || 0;
      const end = start + (idOrQuery.limit || result.length);
      return result.slice(start, end);
    }
    console.log(`๐Ÿ“š Getting all users`);
    return users;
  }
}

const db = new Database();

// ๐ŸŽฏ Each call has the correct return type!
const userById = db.find(1);                    // User | undefined
const userByEmail = db.find("[email protected]"); // User | undefined
const admins = db.find({ role: "admin" });      // User[]
const paginated = db.find({ limit: 2 });        // User[]
const allUsers = db.find();                     // User[]

// TypeScript prevents invalid calls
// db.find({ invalid: true }); // Error! โŒ

๐ŸŽฎ Example 3: Game Action System

Letโ€™s build a flexible game action handler:

// ๐ŸŽฎ Different action types with specific payloads
type MoveAction = { type: "move"; x: number; y: number };
type AttackAction = { type: "attack"; target: string; damage: number };
type HealAction = { type: "heal"; amount: number };
type ChatAction = { type: "chat"; message: string; channel?: string };

interface ActionResult<T> {
  success: boolean;
  data?: T;
  message: string;
  emoji: string;
}

class GameEngine {
  // ๐ŸŽฏ Overloads for each action type
  performAction(action: MoveAction): ActionResult<{ newX: number; newY: number }>;
  performAction(action: AttackAction): ActionResult<{ damageDealt: number }>;
  performAction(action: HealAction): ActionResult<{ healthRestored: number }>;
  performAction(action: ChatAction): ActionResult<{ timestamp: Date }>;
  
  performAction(
    action: MoveAction | AttackAction | HealAction | ChatAction
  ): ActionResult<any> {
    switch (action.type) {
      case "move":
        console.log(`๐Ÿƒ Moving to (${action.x}, ${action.y})`);
        return {
          success: true,
          data: { newX: action.x, newY: action.y },
          message: `Moved to position (${action.x}, ${action.y})`,
          emoji: "๐Ÿƒ"
        };
        
      case "attack":
        console.log(`โš”๏ธ Attacking ${action.target} for ${action.damage} damage!`);
        return {
          success: true,
          data: { damageDealt: action.damage },
          message: `Dealt ${action.damage} damage to ${action.target}!`,
          emoji: "โš”๏ธ"
        };
        
      case "heal":
        console.log(`๐Ÿ’š Healing for ${action.amount} HP`);
        return {
          success: true,
          data: { healthRestored: action.amount },
          message: `Restored ${action.amount} health points`,
          emoji: "๐Ÿ’š"
        };
        
      case "chat":
        const channel = action.channel || "general";
        console.log(`๐Ÿ’ฌ [${channel}] ${action.message}`);
        return {
          success: true,
          data: { timestamp: new Date() },
          message: `Message sent to ${channel}`,
          emoji: "๐Ÿ’ฌ"
        };
    }
  }
}

const game = new GameEngine();

// ๐ŸŽฏ TypeScript knows the exact return type for each action!
const moveResult = game.performAction({ type: "move", x: 10, y: 20 });
// moveResult.data is { newX: number; newY: number }

const attackResult = game.performAction({ type: "attack", target: "Dragon", damage: 50 });
// attackResult.data is { damageDealt: number }

const healResult = game.performAction({ type: "heal", amount: 25 });
// healResult.data is { healthRestored: number }

const chatResult = game.performAction({ type: "chat", message: "Hello team!", channel: "team" });
// chatResult.data is { timestamp: Date }

console.log(`\n${moveResult.emoji} ${moveResult.message}`);
console.log(`${attackResult.emoji} ${attackResult.message}`);
console.log(`${healResult.emoji} ${healResult.message}`);
console.log(`${chatResult.emoji} ${chatResult.message}`);

๐Ÿš€ Advanced Concepts

๐Ÿง™โ€โ™‚๏ธ Generic Function Overloads

Combine generics with overloading for ultimate flexibility:

// ๐ŸŽฏ Generic overloads with constraints
function transform<T extends string>(value: T): Uppercase<T>;
function transform<T extends number>(value: T): number;
function transform<T extends boolean>(value: T): T;
function transform<T extends any[]>(value: T): T["length"];
function transform<T extends object>(value: T): keyof T;

function transform<T>(
  value: T
): Uppercase<string> | number | boolean | number | keyof T {
  if (typeof value === "string") {
    return value.toUpperCase() as Uppercase<string>;
  } else if (typeof value === "number") {
    return value * value;
  } else if (typeof value === "boolean") {
    return value;
  } else if (Array.isArray(value)) {
    return value.length;
  } else {
    return Object.keys(value)[0] as keyof T;
  }
}

// ๐ŸŽฎ Type-safe transformations!
const upper = transform("hello");        // "HELLO" (type: Uppercase<"hello">)
const squared = transform(5);            // 25
const bool = transform(true);            // true
const len = transform([1, 2, 3]);        // 3
const key = transform({ x: 10, y: 20 }); // "x" | "y"

console.log("๐ŸŽจ Transformations:");
console.log(`String: ${upper}`);
console.log(`Number: ${squared}`);
console.log(`Boolean: ${bool}`);
console.log(`Array length: ${len}`);
console.log(`Object key: ${key}`);

๐Ÿ—๏ธ Method Overloading in Classes

Overload methods for powerful class APIs:

// ๐Ÿ“ฆ Storage system with overloaded methods
class TypedStorage {
  private data = new Map<string, any>();
  
  // ๐ŸŽฏ Overloaded set method
  set(key: "theme", value: "light" | "dark"): void;
  set(key: "fontSize", value: number): void;
  set(key: "user", value: { name: string; id: number }): void;
  set<T>(key: string, value: T): void;
  
  set(key: string, value: any): void {
    this.data.set(key, value);
    console.log(`๐Ÿ’พ Stored ${key}: ${JSON.stringify(value)}`);
  }
  
  // ๐ŸŽฏ Overloaded get method
  get(key: "theme"): "light" | "dark" | undefined;
  get(key: "fontSize"): number | undefined;
  get(key: "user"): { name: string; id: number } | undefined;
  get<T>(key: string): T | undefined;
  
  get(key: string): any {
    const value = this.data.get(key);
    console.log(`๐Ÿ“‹ Retrieved ${key}: ${JSON.stringify(value)}`);
    return value;
  }
  
  // ๐ŸŽฏ Batch operations with overloads
  getMany(): Map<string, any>;
  getMany<K extends "theme" | "fontSize" | "user">(...keys: K[]): Pick<{
    theme: "light" | "dark";
    fontSize: number;
    user: { name: string; id: number };
  }, K>;
  
  getMany(...keys: string[]): any {
    if (keys.length === 0) {
      return new Map(this.data);
    }
    
    const result: any = {};
    keys.forEach(key => {
      result[key] = this.data.get(key);
    });
    return result;
  }
}

const storage = new TypedStorage();

// ๐ŸŽฏ Type-safe storage operations
storage.set("theme", "dark");        // Only "light" | "dark" allowed
storage.set("fontSize", 16);         // Only numbers allowed
storage.set("user", { name: "Alice", id: 1 }); // Specific object shape

const theme = storage.get("theme");  // "light" | "dark" | undefined
const size = storage.get("fontSize"); // number | undefined
const user = storage.get("user");    // { name: string; id: number } | undefined

// Batch operations
const selected = storage.getMany("theme", "fontSize");
// Type: { theme: "light" | "dark"; fontSize: number }

๐ŸŽจ Conditional Return Types

Use conditional types with overloads:

// ๐Ÿงฌ Advanced type conditionals
type AsyncResult<T> = T extends Promise<infer U> ? U : T;

interface FetchOptions {
  async?: boolean;
  cache?: boolean;
}

class DataFetcher {
  // ๐ŸŽฏ Overloads with conditional returns
  fetch<T>(url: string, options: { async: true }): Promise<T>;
  fetch<T>(url: string, options: { async: false }): T;
  fetch<T>(url: string, options?: FetchOptions): T | Promise<T>;
  
  fetch<T>(
    url: string,
    options: FetchOptions = {}
  ): T | Promise<T> {
    console.log(`๐ŸŒ Fetching: ${url}`);
    
    const mockData = { id: 1, value: "test" } as T;
    
    if (options.async === true) {
      return new Promise<T>(resolve => {
        setTimeout(() => {
          console.log(`โœ… Async fetch complete`);
          resolve(mockData);
        }, 100);
      });
    } else {
      console.log(`โšก Sync fetch complete`);
      return mockData;
    }
  }
  
  // ๐ŸŽฏ Overloaded batch fetch
  fetchMany<T extends string>(...urls: T[]): Record<T, any>;
  fetchMany<T extends string>(
    options: { async: true },
    ...urls: T[]
  ): Promise<Record<T, any>>;
  
  fetchMany<T extends string>(
    ...args: any[]
  ): Record<T, any> | Promise<Record<T, any>> {
    const isAsync = typeof args[0] === "object" && args[0].async;
    const urls = isAsync ? args.slice(1) : args;
    
    console.log(`๐Ÿ“ฆ Batch fetching ${urls.length} URLs`);
    
    if (isAsync) {
      return Promise.resolve(
        urls.reduce((acc, url) => ({ ...acc, [url]: { data: url } }), {})
      );
    }
    
    return urls.reduce((acc, url) => ({ ...acc, [url]: { data: url } }), {});
  }
}

const fetcher = new DataFetcher();

// ๐ŸŽฏ Return type depends on options!
const syncData = fetcher.fetch<{ id: number }>("api/sync", { async: false });
// Type: { id: number }

const asyncData = fetcher.fetch<{ id: number }>("api/async", { async: true });
// Type: Promise<{ id: number }>

// Handle async result
asyncData.then(data => {
  console.log("๐Ÿ“จ Received async data:", data);
});

โš ๏ธ Common Pitfalls and Solutions

๐Ÿ˜ฑ Pitfall 1: Implementation Signature Visibility

// โŒ Wrong - implementation signature is NOT callable!
function processData(x: number): string;
function processData(x: string): number;
function processData(x: number | string): string | number {
  return typeof x === "number" ? String(x) : x.length;
}

// User CANNOT call this:
// processData(true); // Error! boolean not in overloads โœ…

// โœ… Correct - overloads define the public API
// The implementation signature is hidden from users

๐Ÿคฏ Pitfall 2: Incorrect Overload Order

// โŒ Wrong - general overload blocks specific ones
function parseValue(value: any): string;          // Too general!
function parseValue(value: number): number;       // Never reached
function parseValue(value: boolean): boolean;     // Never reached
function parseValue(value: any): any {
  return value;
}

// โœ… Correct - specific to general
function parseValue(value: number): number;
function parseValue(value: boolean): boolean;
function parseValue(value: any): string;     // General case last
function parseValue(value: any): any {
  if (typeof value === "number") return value;
  if (typeof value === "boolean") return value;
  return String(value);
}

const num = parseValue(42);      // number โœ…
const bool = parseValue(true);   // boolean โœ…
const str = parseValue({});      // string โœ…

๐Ÿ˜ต Pitfall 3: Incompatible Overloads

// โŒ Wrong - overloads must be compatible with implementation
function badFunction(x: string): number;
function badFunction(x: number): string;
function badFunction(x: boolean): boolean; // Error! boolean not in implementation
function badFunction(x: string | number): string | number {
  return typeof x === "string" ? x.length : String(x);
}

// โœ… Correct - all overloads match implementation
function goodFunction(x: string): number;
function goodFunction(x: number): string;
function goodFunction(x: string | number): string | number {
  return typeof x === "string" ? x.length : String(x);
}

๐Ÿค” Pitfall 4: Overload vs Union Types

// ๐Ÿค” When to use overloads vs union types?

// โŒ Unnecessary overload - union type is simpler
function getId(user: { id: number }): number;
function getId(product: { id: number }): number;
function getId(item: { id: number }): number {
  return item.id;
}

// โœ… Better - just use union or base type
function getId(item: { id: number }): number {
  return item.id;
}

// โœ… Good use of overloads - different return types
function process(data: string): string[];
function process(data: number): number;
function process(data: string | number): string[] | number {
  return typeof data === "string" ? data.split("") : data * 2;
}

๐Ÿ˜ฌ Pitfall 5: this Parameter in Overloads

// โš ๏ธ Tricky - this parameter in overloads
class Counter {
  private count = 0;
  
  // โŒ Wrong - this parameter position
  increment(this: Counter, amount: number): this;
  increment(amount?: number): this; // Error! Incompatible
  
  increment(amount: number = 1): this {
    this.count += amount;
    return this;
  }
}

// โœ… Correct - consistent this parameter
class GoodCounter {
  private count = 0;
  
  increment(): this;
  increment(amount: number): this;
  increment(amount?: number): this {
    this.count += amount ?? 1;
    console.log(`๐Ÿ”ข Count: ${this.count}`);
    return this;
  }
}

const counter = new GoodCounter();
counter.increment().increment(5).increment(); // Chaining works!

๐Ÿ› ๏ธ Best Practices

1. ๐ŸŽฏ Order Overloads Correctly

// โœ… Most specific โ†’ least specific
function connect(port: 3000): { secure: true };
function connect(port: 8080): { secure: false };
function connect(port: number): { secure: boolean };
function connect(host: string, port: number): { secure: boolean };

function connect(
  portOrHost: number | string,
  port?: number
): { secure: boolean } {
  // Implementation
  return { secure: portOrHost === 3000 };
}

2. ๐Ÿ“ Write Clear Overload Signatures

// โœ… Good - clear what each overload does
/**
 * Creates a date from various inputs
 */
function createDate(): Date;                    // Current date
function createDate(timestamp: number): Date;   // From timestamp
function createDate(dateString: string): Date;  // From string
function createDate(year: number, month: number, day: number): Date; // From parts

function createDate(
  yearOrValue?: number | string,
  month?: number,
  day?: number
): Date {
  if (yearOrValue === undefined) {
    return new Date();
  } else if (typeof yearOrValue === "string") {
    return new Date(yearOrValue);
  } else if (month !== undefined && day !== undefined) {
    return new Date(yearOrValue, month - 1, day); // month is 0-indexed
  } else {
    return new Date(yearOrValue);
  }
}

3. ๐Ÿ›ก๏ธ Use Type Guards in Implementation

// โœ… Good - clear type guards
function process(data: string): string[];
function process(data: number[]): number;
function process(data: { values: number[] }): number[];

function process(
  data: string | number[] | { values: number[] }
): string[] | number | number[] {
  if (typeof data === "string") {
    return data.split("");
  } else if (Array.isArray(data)) {
    return data.reduce((a, b) => a + b, 0);
  } else {
    return data.values.map(v => v * 2);
  }
}

4. ๐ŸŽจ Consider Generic Overloads

// โœ… Good - flexible with generics
function firstElement<T>(arr: T[]): T | undefined;
function firstElement<T>(arr: T[], defaultValue: T): T;

function firstElement<T>(
  arr: T[],
  defaultValue?: T
): T | undefined {
  return arr.length > 0 ? arr[0] : defaultValue;
}

const first1 = firstElement([1, 2, 3]);        // number | undefined
const first2 = firstElement([1, 2, 3], 0);     // number (never undefined!)
const first3 = firstElement([], "default");     // string

5. โœจ Keep Implementation Simple

// โœ… Good - clean implementation
interface FormatOptions {
  uppercase?: boolean;
  prefix?: string;
}

function format(value: number): string;
function format(value: string, options?: FormatOptions): string;
function format(value: Date, format: string): string;

function format(
  value: number | string | Date,
  optionsOrFormat?: FormatOptions | string
): string {
  // Delegate to specific handlers
  if (typeof value === "number") {
    return formatNumber(value);
  } else if (value instanceof Date) {
    return formatDate(value, optionsOrFormat as string);
  } else {
    return formatString(value, optionsOrFormat as FormatOptions);
  }
}

// Helper functions keep implementation clean
function formatNumber(n: number): string {
  return `$${n.toFixed(2)}`;
}

function formatString(s: string, options?: FormatOptions): string {
  let result = options?.prefix ? options.prefix + s : s;
  return options?.uppercase ? result.toUpperCase() : result;
}

function formatDate(d: Date, format: string): string {
  // Simple date formatting
  return d.toLocaleDateString();
}

๐Ÿงช Hands-On Exercise

๐ŸŽฏ Challenge: Build a Flexible HTTP Client

Create a type-safe HTTP client with overloaded methods:

๐Ÿ“‹ Requirements:

  • โœ… Support GET, POST, PUT, DELETE with proper types
  • ๐Ÿท๏ธ Different return types based on endpoint
  • ๐Ÿ‘ค Authentication handling
  • ๐Ÿ“… Request/Response interceptors
  • ๐ŸŽจ Type-safe error handling

๐Ÿš€ Bonus Points:

  • Add request caching
  • Implement retry logic
  • Create middleware system

๐Ÿ’ก Solution

๐Ÿ” Click to see solution
// ๐ŸŽฏ Flexible HTTP client with overloads
interface User {
  id: number;
  name: string;
  email: string;
}

interface Post {
  id: number;
  title: string;
  content: string;
  authorId: number;
}

interface ApiError {
  code: number;
  message: string;
}

type ApiResponse<T> = 
  | { success: true; data: T }
  | { success: false; error: ApiError };

class HttpClient {
  constructor(private baseUrl: string) {}
  
  // ๐ŸŽฏ GET overloads
  get(endpoint: "/users"): Promise<ApiResponse<User[]>>;
  get(endpoint: `/users/${number}`): Promise<ApiResponse<User>>;
  get(endpoint: "/posts"): Promise<ApiResponse<Post[]>>;
  get(endpoint: `/posts/${number}`): Promise<ApiResponse<Post>>;
  get<T>(endpoint: string): Promise<ApiResponse<T>>;
  
  async get<T>(endpoint: string): Promise<ApiResponse<T>> {
    try {
      console.log(`๐ŸŒ GET ${this.baseUrl}${endpoint}`);
      
      // Simulate API call
      await this.delay(100);
      
      // Mock responses based on endpoint
      if (endpoint === "/users") {
        return {
          success: true,
          data: [
            { id: 1, name: "Alice", email: "[email protected]" },
            { id: 2, name: "Bob", email: "[email protected]" }
          ] as any
        };
      } else if (endpoint.startsWith("/users/")) {
        return {
          success: true,
          data: { id: 1, name: "Alice", email: "[email protected]" } as any
        };
      } else if (endpoint === "/posts") {
        return {
          success: true,
          data: [
            { id: 1, title: "Hello", content: "World", authorId: 1 }
          ] as any
        };
      }
      
      return { success: true, data: {} as any };
    } catch (error) {
      return {
        success: false,
        error: { code: 500, message: "Internal Server Error" }
      };
    }
  }
  
  // ๐ŸŽฏ POST overloads
  post(endpoint: "/users", data: Omit<User, "id">): Promise<ApiResponse<User>>;
  post(endpoint: "/posts", data: Omit<Post, "id">): Promise<ApiResponse<Post>>;
  post<T, R>(endpoint: string, data: T): Promise<ApiResponse<R>>;
  
  async post<T, R>(
    endpoint: string,
    data: T
  ): Promise<ApiResponse<R>> {
    try {
      console.log(`๐Ÿ“จ POST ${this.baseUrl}${endpoint}`, data);
      await this.delay(150);
      
      // Mock response
      if (endpoint === "/users") {
        return {
          success: true,
          data: { id: Date.now(), ...data } as any
        };
      } else if (endpoint === "/posts") {
        return {
          success: true,
          data: { id: Date.now(), ...data } as any
        };
      }
      
      return { success: true, data: data as any };
    } catch (error) {
      return {
        success: false,
        error: { code: 500, message: "Failed to create resource" }
      };
    }
  }
  
  // ๐ŸŽฏ PUT overloads
  put(endpoint: `/users/${number}`, data: Partial<User>): Promise<ApiResponse<User>>;
  put(endpoint: `/posts/${number}`, data: Partial<Post>): Promise<ApiResponse<Post>>;
  put<T, R>(endpoint: string, data: T): Promise<ApiResponse<R>>;
  
  async put<T, R>(
    endpoint: string,
    data: T
  ): Promise<ApiResponse<R>> {
    try {
      console.log(`๐Ÿ”„ PUT ${this.baseUrl}${endpoint}`, data);
      await this.delay(150);
      
      return {
        success: true,
        data: { id: 1, ...data } as any
      };
    } catch (error) {
      return {
        success: false,
        error: { code: 500, message: "Failed to update resource" }
      };
    }
  }
  
  // ๐ŸŽฏ DELETE overloads
  delete(endpoint: `/users/${number}`): Promise<ApiResponse<void>>;
  delete(endpoint: `/posts/${number}`): Promise<ApiResponse<void>>;
  delete(endpoint: string): Promise<ApiResponse<void>>;
  
  async delete(endpoint: string): Promise<ApiResponse<void>> {
    try {
      console.log(`๐Ÿ—‘๏ธ DELETE ${this.baseUrl}${endpoint}`);
      await this.delay(100);
      
      return { success: true, data: undefined as any };
    } catch (error) {
      return {
        success: false,
        error: { code: 500, message: "Failed to delete resource" }
      };
    }
  }
  
  // ๐ŸŽฏ Batch operations
  batch<T extends readonly string[]>(
    ...endpoints: T
  ): Promise<{ [K in keyof T]: ApiResponse<any> }>;
  
  async batch<T extends readonly string[]>(
    ...endpoints: T
  ): Promise<{ [K in keyof T]: ApiResponse<any> }> {
    console.log(`๐Ÿ“ฆ Batch request for ${endpoints.length} endpoints`);
    
    const results = await Promise.all(
      endpoints.map(endpoint => this.get(endpoint))
    );
    
    return results as { [K in keyof T]: ApiResponse<any> };
  }
  
  private delay(ms: number): Promise<void> {
    return new Promise(resolve => setTimeout(resolve, ms));
  }
}

// ๐ŸŽฎ Usage
const api = new HttpClient("https://api.example.com");

// Type-safe API calls
async function demo() {
  // GET requests with proper types
  const usersResult = await api.get("/users");        // ApiResponse<User[]>
  const userResult = await api.get("/users/1");       // ApiResponse<User>
  const postsResult = await api.get("/posts");        // ApiResponse<Post[]>
  
  if (usersResult.success) {
    console.log("๐Ÿ‘ฅ Users:", usersResult.data.map(u => u.name));
  }
  
  // POST with type checking
  const newUserResult = await api.post("/users", {
    name: "Charlie",
    email: "[email protected]"
  }); // ApiResponse<User>
  
  if (newUserResult.success) {
    console.log("โœ… Created user:", newUserResult.data);
  }
  
  // Batch operations
  const batchResult = await api.batch("/users", "/posts");
  // Type: [ApiResponse<User[]>, ApiResponse<Post[]>]
  
  console.log("๐Ÿ“ฆ Batch results:", batchResult);
}

// Run demo
demo();

๐ŸŽ“ Key Takeaways

Youโ€™ve mastered function overloading! Hereโ€™s what you can now do:

  • โœ… Create multiple function signatures for flexible APIs ๐Ÿ’ช
  • โœ… Order overloads correctly from specific to general ๐Ÿ›ก๏ธ
  • โœ… Combine with generics for maximum power ๐ŸŽฏ
  • โœ… Build type-safe APIs that feel magical to use ๐Ÿ›
  • โœ… Avoid common pitfalls like implementation visibility! ๐Ÿš€

Remember: Function overloading is like giving your functions superpowers - use them wisely! ๐Ÿค

๐Ÿค Next Steps

Congratulations! ๐ŸŽ‰ Youโ€™ve mastered function overloading!

Hereโ€™s what to do next:

  1. ๐Ÿ’ป Complete the HTTP client exercise above
  2. ๐Ÿ—๏ธ Refactor existing functions to use overloads
  3. ๐Ÿ“š Move on to our next tutorial: Arrow Functions in TypeScript
  4. ๐ŸŒŸ Create a library with beautifully overloaded APIs!

Remember: Great APIs feel intuitive to use but are precise underneath. Function overloading helps you achieve both! ๐Ÿš€

Happy coding! ๐ŸŽ‰๐Ÿš€โœจ