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:
- Flexible APIs ๐: One function, multiple ways to use it
- Type Precision ๐ฏ: Each call gets exact type checking
- Better IntelliSense ๐ป: IDE shows all available signatures
- Self-Documenting ๐: Signatures show all usage patterns
- 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:
- ๐ป Complete the HTTP client exercise above
- ๐๏ธ Refactor existing functions to use overloads
- ๐ Move on to our next tutorial: Arrow Functions in TypeScript
- ๐ 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! ๐๐โจ