TypeScript Fundamentals
Course 1 ยท Chapter 8 ยท Functions & Overloads
๐ Functions & Overloads
You've written typed functions. Now we'll explore how TypeScript handles functions that can behave differently based on their arguments. Function overloads let you define multiple signatures for a single function, making it type-safe and self-documenting.
๐ง Function Typing Basics (Review)
Parameter Types and Return Types
function greet(name: string): string {
return `Hello, ${name}!`;
}
function add(a: number, b: number): number {
return a + b;
}
const multiply = (a: number, b: number): number => a * b;
function greetOptional(name: string, title?: string): string {
return title ? `${title} ${name}` : name;
}
function sum(...numbers: number[]): number {
return numbers.reduce((a, b) => a + b, 0);
}
๐ฏ Function Overloads
A function overload lets you define multiple signatures for the same function. The actual implementation comes after all the signatures. TypeScript uses these signatures to type-check calls to the function.
The Pattern
function describe(value: string): string;
function describe(value: number): string;
function describe(value: string | number): string {
if (typeof value === "string") {
return `String: ${value}`;
} else {
return `Number: ${value.toFixed(2)}`;
}
}
describe("hello");
describe(42);
describe(true);
โ
Key Point: The overload signatures are for type-checking. The implementation uses a union type that covers all cases.
Overloads with Different Return Types
function process(input: string): string;
function process(input: number): number;
function process(input: boolean): boolean;
function process(input: string | number | boolean) {
if (typeof input === "string") {
return input.toUpperCase();
} else if (typeof input === "number") {
return input * 2;
} else {
return !input;
}
}
const str: string = process("hello");
const num: number = process(5);
const bool: boolean = process(true);
Overloads with Optional Parameters
function createElement(tag: string): HTMLElement;
function createElement(tag: string, content: string): HTMLElement;
function createElement(tag: string, content: string, className: string): HTMLElement;
function createElement(
tag: string,
content?: string,
className?: string
): HTMLElement {
const el = document.createElement(tag);
if (content) el.textContent = content;
if (className) el.className = className;
return el;
}
createElement("div");
createElement("div", "Hello");
createElement("div", "Hello", "container");
๐ค Overloads vs Union Types
When should you use overloads instead of union types? Let's compare:
โ Union Type (Bad)
function padString(
value: string | number
): string {
return String(value).padStart(10);
}
const result = padString(42);
โ
Overloads (Good)
function padString(value: string): string;
function padString(value: number): string;
function padString(value: string | number) {
return String(value).padStart(10);
}
const result = padString(42);
When to Use Overloads
- Different return types โ Based on input type, return type differs
- Different parameters โ Accept different number of parameters
- Complex relationships โ Type of parameter 2 depends on parameter 1
- API clarity โ Make intent clear to callers
When NOT to Use Overloads
- Same behavior โ If logic is identical, just use union types
- Simple cases โ Optional parameters are often enough
- Too many overloads โ More than 3-4 usually signals design problem
๐ Advanced Overload Patterns
Overloads with Generics
function getProperty<T>(obj: T, key: string): unknown;
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K];
function getProperty<T>(obj: T, key: string) {
return obj[key as keyof T];
}
interface User {
name: string;
age: number;
}
const user: User = { name: "Alice", age: 30 };
const name: string = getProperty(user, "name");
const age: number = getProperty(user, "age");
Overloads with Conditional Return Types
function flatten<T>(arr: T[]): T[];
function flatten<T>(arr: T[][]): T[];
function flatten<T>(arr: T[] | T[][]): T[] {
return arr.flat() as T[];
}
const single: string[] = flatten(["a", "b"]);
const double: number[] = flatten([[1, 2], [3, 4]]);
๐ Best Practices
โ
DO: Overloads for Clearer Intent
When different input types produce different return types, overloads document this relationship:
function parse(input: string): Record<string, unknown>;
function parse(input: Buffer): Uint8Array;
function parse(input: string | Buffer) {
}
โ DON'T: Too Many Overloads
More than 3-4 overloads often means your function is doing too much:
function process(input: string): string;
function process(input: number): number;
function process(input: boolean): boolean;
function process(input: Date): string;
function process(input: object): object;
๐ก Tip: Overloads Should Be Simple
If the overload signatures are hard to understand, the function signature might be too complex. Consider breaking it into multiple, simpler functions instead.
๐ป Coding Challenges
Challenge 1: Basic Function Overload
Create a function that accepts either a string or a number and returns a boolean. If string, check if length > 5. If number, check if > 100.
- Define two overload signatures
- Write the implementation with both cases
- Test with both string and number inputs
Goal: Practice basic overload syntax and type checking.
โ Solution
Challenge 2: Overload with Different Return Types
Create a convertToJSON function that takes an object and optional formatting. If formatting is true, return pretty JSON. If false, return compact JSON.
- Define overloads for formatted/unformatted
- Both return strings (different content)
- Use JSON.stringify with appropriate parameters
Goal: Practice conditional behavior with overloads.
โ Solution
Challenge 3: Generic Overload
Create a wrapInArray function that takes a single value OR an array, and always returns an array. If passed a single value, wrap it. If passed an array, return it as-is.
- Define overloads for single value and array
- Use generics to preserve the element type
- Ensure correct array nesting
Goal: Practice generic overloads and type preservation.
โ Solution
๐ Real-World Example: Event Listener
function addEventListener(
element: HTMLElement,
event: "click",
handler: (e: MouseEvent) => void
): void;
function addEventListener(
element: HTMLElement,
event: "input",
handler: (e: InputEvent) => void
): void;
function addEventListener(
element: HTMLElement,
event: "click" | "input",
handler: (e: Event) => void
) {
element.addEventListener(event, handler);
}
const button = document.querySelector("button")!;
const input = document.querySelector("input")!;
addEventListener(button, "click", (e: MouseEvent) => {
console.log(e.clientX);
});
addEventListener(input, "input", (e: InputEvent) => {
console.log(e.data);
});
๐ฏ What's Next
We've mastered function overloads and learned when to use them. Chapter 9 covers Enums & Literal Types โ how to create strongly-typed constants and restrict values to specific options.