Callbacks
Introduction
Callbacks are functions passed as arguments to other functions. They're a fundamental pattern in JavaScript and TypeScript for handling asynchronous operations, event handling, and creating flexible, reusable code.
In TypeScript, callbacks benefit from type safety - you can specify exactly what parameters the callback receives and what it should return. This prevents many common bugs that occur in JavaScript.
Why Callbacks Matter
Without callbacks (repetitive):
function processNumbers1(): void {
console.log(1 * 2);
console.log(2 * 2);
console.log(3 * 2);
}
function processNumbers2(): void {
console.log(1 + 10);
console.log(2 + 10);
console.log(3 + 10);
}
// Need separate function for each operation!
With callbacks (flexible):
function processNumbers(callback: (n: number) => number): void {
console.log(callback(1));
console.log(callback(2));
console.log(callback(3));
}
processNumbers((n) => n * 2); // Double
processNumbers((n) => n + 10); // Add 10
// One function, many operations!
Callbacks in Real Applications
Callbacks are everywhere in JavaScript/TypeScript:
- Asynchronous operations - File reading, API calls, timers
- Event handlers - Button clicks, keyboard input
- Array methods -
map(),filter(),forEach() - Higher-order functions - Functions that operate on other functions
Concepts
Basic Callback
A callback is simply a function passed as an argument:
function greet(name: string, callback: (message: string) => void): void {
const message = `Hello, ${name}!`;
callback(message);
}
greet("Alice", (msg) => {
console.log(msg); // "Hello, Alice!"
});
greet("Bob", (msg) => {
console.log(msg.toUpperCase()); // "HELLO, BOB!"
});
Callback with Multiple Parameters
function calculate(
a: number,
b: number,
operation: (x: number, y: number) => number
): number {
return operation(a, b);
}
const sum = calculate(5, 3, (x, y) => x + y); // 8
const product = calculate(5, 3, (x, y) => x * y); // 15
const difference = calculate(5, 3, (x, y) => x - y); // 2
console.log(sum, product, difference);
Callback with Return Value
The callback can return a value that the main function uses:
function processArray<T, U>(
arr: T[],
callback: (item: T, index: number) => U
): U[] {
const result: U[] = [];
for (let i = 0; i < arr.length; i++) {
result.push(callback(arr[i], i));
}
return result;
}
const numbers = [1, 2, 3, 4];
const doubled = processArray(numbers, (n) => n * 2);
const strings = processArray(numbers, (n, i) => `${i}: ${n}`);
console.log(doubled); // [2, 4, 6, 8]
console.log(strings); // ["0: 1", "1: 2", "2: 3", "3: 4"]
Asynchronous Callbacks
Callbacks are commonly used for asynchronous operations:
function fetchData(callback: (data: string) => void): void {
console.log("Fetching data...");
setTimeout(() => {
const data = "Data loaded from server";
callback(data);
}, 2000);
}
console.log("Start");
fetchData((data) => {
console.log(data); // After 2 seconds: "Data loaded from server"
});
console.log("End");
// Output order:
// Start
// Fetching data...
// End
// (2 seconds later) Data loaded from server
Error-First Callbacks (Node.js Pattern)
A common pattern is to pass an error as the first parameter:
function readFile(
filename: string,
callback: (error: Error | null, data: string | null) => void
): void {
setTimeout(() => {
if (filename.endsWith(".txt")) {
callback(null, "File contents here");
} else {
callback(new Error("Invalid file type"), null);
}
}, 1000);
}
// Usage
readFile("document.txt", (error, data) => {
if (error) {
console.log("Error:", error.message);
} else {
console.log("Data:", data);
}
});
Callback Types with Interfaces
You can define callback types using type aliases or interfaces:
type ValidationCallback = (value: string) => boolean;
type TransformCallback<T, U> = (value: T) => U;
interface EventCallback {
(event: string, data: any): void;
}
function validate(input: string, validator: ValidationCallback): boolean {
return validator(input);
}
const isEmail: ValidationCallback = (value) => value.includes("@");
const isNotEmpty: ValidationCallback = (value) => value.length > 0;
console.log(validate("test@example.com", isEmail)); // true
console.log(validate("", isNotEmpty)); // false
Examples
Array Processing with Callbacks
class ArrayProcessor {
static map<T, U>(arr: T[], callback: (item: T, index: number) => U): U[] {
const result: U[] = [];
for (let i = 0; i < arr.length; i++) {
result.push(callback(arr[i], i));
}
return result;
}
static filter<T>(arr: T[], predicate: (item: T) => boolean): T[] {
const result: T[] = [];
for (const item of arr) {
if (predicate(item)) {
result.push(item);
}
}
return result;
}
static forEach<T>(arr: T[], callback: (item: T, index: number) => void): void {
for (let i = 0; i < arr.length; i++) {
callback(arr[i], i);
}
}
static reduce<T, U>(
arr: T[],
callback: (accumulator: U, current: T) => U,
initialValue: U
): U {
let accumulator = initialValue;
for (const item of arr) {
accumulator = callback(accumulator, item);
}
return accumulator;
}
}
// Usage
const numbers = [1, 2, 3, 4, 5];
const doubled = ArrayProcessor.map(numbers, (n) => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
const evens = ArrayProcessor.filter(numbers, (n) => n % 2 === 0);
console.log(evens); // [2, 4]
ArrayProcessor.forEach(numbers, (n, i) => {
console.log(`Index ${i}: ${n}`);
});
const sum = ArrayProcessor.reduce(numbers, (acc, n) => acc + n, 0);
console.log(sum); // 15
Event System with Callbacks
type EventCallback = (data: any) => void;
class EventEmitter {
private events: Map<string, EventCallback[]> = new Map();
on(event: string, callback: EventCallback): void {
if (!this.events.has(event)) {
this.events.set(event, []);
}
this.events.get(event)!.push(callback);
}
off(event: string, callback: EventCallback): void {
const callbacks = this.events.get(event);
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
}
emit(event: string, data?: any): void {
const callbacks = this.events.get(event);
if (callbacks) {
callbacks.forEach(callback => callback(data));
}
}
once(event: string, callback: EventCallback): void {
const onceWrapper = (data: any) => {
callback(data);
this.off(event, onceWrapper);
};
this.on(event, onceWrapper);
}
}
// Usage
const emitter = new EventEmitter();
// Register multiple listeners
emitter.on("userLogin", (username: string) => {
console.log(`${username} logged in`);
});
emitter.on("userLogin", (username: string) => {
console.log(`Welcome back, ${username}!`);
});
emitter.on("userLogin", (username: string) => {
console.log(`Checking profile for ${username}...`);
});
// Fire event
emitter.emit("userLogin", "Alice");
// Output:
// Alice logged in
// Welcome back, Alice!
// Checking profile for Alice...
// One-time listener
emitter.once("appShutdown", () => {
console.log("Cleaning up...");
});
emitter.emit("appShutdown"); // Cleaning up...
emitter.emit("appShutdown"); // (nothing happens - only once)
Simulating Asynchronous File Operations
interface FileData {
name: string;
size: number;
content: string;
}
type FileCallback = (error: Error | null, data: FileData | null) => void;
class FileSystem {
static readFile(filename: string, callback: FileCallback): void {
console.log(`Reading ${filename}...`);
setTimeout(() => {
if (filename.endsWith(".txt")) {
callback(null, {
name: filename,
size: 1024,
content: "File contents here"
});
} else {
callback(new Error("Invalid file type"), null);
}
}, 1000);
}
static writeFile(
filename: string,
content: string,
callback: (error: Error | null) => void
): void {
console.log(`Writing to ${filename}...`);
setTimeout(() => {
if (content.length > 0) {
console.log(`Wrote ${content.length} characters`);
callback(null);
} else {
callback(new Error("Cannot write empty file"));
}
}, 500);
}
static deleteFile(
filename: string,
callback: (error: Error | null) => void
): void {
console.log(`Deleting ${filename}...`);
setTimeout(() => {
callback(null);
console.log(`${filename} deleted`);
}, 300);
}
}
// Usage
FileSystem.readFile("document.txt", (error, data) => {
if (error) {
console.log("Error reading file:", error.message);
} else {
console.log("File:", data?.name);
console.log("Size:", data?.size);
console.log("Content:", data?.content);
}
});
FileSystem.writeFile("output.txt", "Hello World", (error) => {
if (error) {
console.log("Error writing file:", error.message);
} else {
console.log("File written successfully");
}
});
Timer and Scheduling
class Timer {
static delay(ms: number, callback: () => void): void {
setTimeout(callback, ms);
}
static repeat(
ms: number,
callback: (iteration: number) => void,
maxIterations?: number
): () => void {
let iteration = 0;
const intervalId = setInterval(() => {
iteration++;
callback(iteration);
if (maxIterations && iteration >= maxIterations) {
clearInterval(intervalId);
}
}, ms);
// Return function to stop the timer
return () => clearInterval(intervalId);
}
static debounce<T extends (...args: any[]) => void>(
callback: T,
delay: number
): T {
let timeoutId: NodeJS.Timeout | null = null;
return ((...args: any[]) => {
if (timeoutId) {
clearTimeout(timeoutId);
}
timeoutId = setTimeout(() => {
callback(...args);
}, delay);
}) as T;
}
static throttle<T extends (...args: any[]) => void>(
callback: T,
delay: number
): T {
let lastCall = 0;
return ((...args: any[]) => {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
callback(...args);
}
}) as T;
}
}
// Usage
Timer.delay(2000, () => {
console.log("Executed after 2 seconds");
});
const stop = Timer.repeat(1000, (iteration) => {
console.log(`Iteration ${iteration}`);
}, 5);
// Debounce example (e.g., search input)
const search = Timer.debounce((query: string) => {
console.log(`Searching for: ${query}`);
}, 500);
search("a");
search("ab");
search("abc"); // Only this will execute after 500ms
// Throttle example (e.g., scroll handler)
const onScroll = Timer.throttle(() => {
console.log("Scroll event");
}, 200);
Challenges
1. Delayed Message
- Create a function
delayedMessage(message, delay, callback) - Wait specified milliseconds, then call callback with message
💡 Show Answer
function delayedMessage(
message: string,
delay: number,
callback: (msg: string) => void
): void {
setTimeout(() => {
callback(message);
}, delay);
}
delayedMessage("Hello!", 2000, (msg) => {
console.log(msg); // "Hello!" after 2 seconds
});
delayedMessage("World!", 1000, (msg) => {
console.log(msg.toUpperCase()); // "WORLD!" after 1 second
});
2. Array Filter Implementation
- Implement a
filterArrayfunction - Takes an array and a predicate callback
- Returns new array with items that pass the test
💡 Show Answer
function filterArray<T>(
arr: T[],
predicate: (item: T, index: number) => boolean
): T[] {
const result: T[] = [];
for (let i = 0; i < arr.length; i++) {
if (predicate(arr[i], i)) {
result.push(arr[i]);
}
}
return result;
}
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const evens = filterArray(numbers, (n) => n % 2 === 0);
console.log(evens); // [2, 4, 6, 8, 10]
const greaterThan5 = filterArray(numbers, (n) => n > 5);
console.log(greaterThan5); // [6, 7, 8, 9, 10]
const evenIndices = filterArray(numbers, (n, i) => i % 2 === 0);
console.log(evenIndices); // [1, 3, 5, 7, 9]
3. Calculator with Callback
- Create a calculator function
- Takes two numbers and an operation callback
- Returns the result
💡 Show Answer
function calculator(
a: number,
b: number,
operation: (x: number, y: number) => number
): number {
return operation(a, b);
}
console.log(calculator(10, 5, (x, y) => x + y)); // 15
console.log(calculator(10, 5, (x, y) => x - y)); // 5
console.log(calculator(10, 5, (x, y) => x * y)); // 50
console.log(calculator(10, 5, (x, y) => x / y)); // 2
console.log(calculator(10, 5, (x, y) => x ** y)); // 100000
// Create reusable operations
const add = (x: number, y: number) => x + y;
const multiply = (x: number, y: number) => x * y;
console.log(calculator(7, 3, add)); // 10
console.log(calculator(7, 3, multiply)); // 21
4. Retry with Callback
- Function that retries an operation
- Call callback on success or after max attempts
- Track attempt count
💡 Show Answer
function retryOperation(
operation: () => boolean,
maxAttempts: number,
onComplete: (success: boolean, attempts: number) => void
): void {
let attempts = 0;
const tryOperation = () => {
attempts++;
console.log(`Attempt ${attempts}...`);
const success = operation();
if (success) {
onComplete(true, attempts);
} else if (attempts >= maxAttempts) {
onComplete(false, attempts);
} else {
setTimeout(tryOperation, 1000);
}
};
tryOperation();
}
// Simulate operation that succeeds after 3 attempts
let attemptCount = 0;
retryOperation(
() => {
attemptCount++;
return attemptCount >= 3; // Succeed on 3rd attempt
},
5,
(success, attempts) => {
if (success) {
console.log(`Operation succeeded after ${attempts} attempts`);
} else {
console.log(`Operation failed after ${attempts} attempts`);
}
}
);
5. Map Function Implementation
- Implement a
mapArrayfunction - Transform array elements using callback
- Return new array with transformed elements
💡 Show Answer
function mapArray<T, U>(
arr: T[],
transform: (item: T, index: number) => U
): U[] {
const result: U[] = [];
for (let i = 0; i < arr.length; i++) {
result.push(transform(arr[i], i));
}
return result;
}
const numbers = [1, 2, 3, 4, 5];
const doubled = mapArray(numbers, (n) => n * 2);
console.log(doubled); // [2, 4, 6, 8, 10]
const squared = mapArray(numbers, (n) => n ** 2);
console.log(squared); // [1, 4, 9, 16, 25]
const indexed = mapArray(numbers, (n, i) => `${i}: ${n}`);
console.log(indexed); // ["0: 1", "1: 2", "2: 3", "3: 4", "4: 5"]
const objects = mapArray(numbers, (n) => ({ value: n, double: n * 2 }));
console.log(objects);
// [{ value: 1, double: 2 }, { value: 2, double: 4 }, ...]
6. ForEach Implementation
- Implement
forEachArrayfunction - Execute callback for each element
- No return value
💡 Show Answer
function forEachArray<T>(
arr: T[],
callback: (item: T, index: number, array: T[]) => void
): void {
for (let i = 0; i < arr.length; i++) {
callback(arr[i], i, arr);
}
}
const fruits = ["apple", "banana", "cherry"];
forEachArray(fruits, (fruit, index) => {
console.log(`${index}: ${fruit}`);
});
// Calculate sum
let sum = 0;
forEachArray([1, 2, 3, 4, 5], (num) => {
sum += num;
});
console.log(`Sum: ${sum}`); // Sum: 15
// Modify elements (though not recommended with forEach)
const numbers = [1, 2, 3];
forEachArray(numbers, (num, index, arr) => {
arr[index] = num * 2;
});
console.log(numbers); // [2, 4, 6]
7. Find Implementation
- Implement
findInArrayfunction - Return first element that passes test
- Return undefined if not found
💡 Show Answer
function findInArray<T>(
arr: T[],
predicate: (item: T, index: number) => boolean
): T | undefined {
for (let i = 0; i < arr.length; i++) {
if (predicate(arr[i], i)) {
return arr[i];
}
}
return undefined;
}
const numbers = [1, 5, 10, 15, 20];
const firstGreaterThan10 = findInArray(numbers, (n) => n > 10);
console.log(firstGreaterThan10); // 15
const firstEven = findInArray(numbers, (n) => n % 2 === 0);
console.log(firstEven); // 10
const notFound = findInArray(numbers, (n) => n > 100);
console.log(notFound); // undefined
// Find object in array
interface User {
id: number;
name: string;
}
const users: User[] = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" },
{ id: 3, name: "Charlie" }
];
const user = findInArray(users, (u) => u.name === "Bob");
console.log(user); // { id: 2, name: "Bob" }
8. Reduce Implementation
- Implement
reduceArrayfunction - Accumulate values using callback
- Return single result
💡 Show Answer
function reduceArray<T, U>(
arr: T[],
callback: (accumulator: U, current: T, index: number) => U,
initialValue: U
): U {
let accumulator = initialValue;
for (let i = 0; i < arr.length; i++) {
accumulator = callback(accumulator, arr[i], i);
}
return accumulator;
}
const numbers = [1, 2, 3, 4, 5];
// Sum
const sum = reduceArray(numbers, (acc, n) => acc + n, 0);
console.log(sum); // 15
// Product
const product = reduceArray(numbers, (acc, n) => acc * n, 1);
console.log(product); // 120
// Max
const max = reduceArray(numbers, (acc, n) => Math.max(acc, n), -Infinity);
console.log(max); // 5
// Build object
const obj = reduceArray(
numbers,
(acc, n) => {
acc[`key${n}`] = n * 2;
return acc;
},
{} as Record<string, number>
);
console.log(obj); // { key1: 2, key2: 4, key3: 6, key4: 8, key5: 10 }
// Count occurrences
const fruits = ["apple", "banana", "apple", "cherry", "banana", "apple"];
const counts = reduceArray(
fruits,
(acc, fruit) => {
acc[fruit] = (acc[fruit] || 0) + 1;
return acc;
},
{} as Record<string, number>
);
console.log(counts); // { apple: 3, banana: 2, cherry: 1 }
9. Async Series Execution
- Execute callbacks in series
- Each callback waits for previous to complete
- Pass result to next callback
💡 Show Answer
function executeSeries(
tasks: Array<(callback: (result: any) => void) => void>,
finalCallback: (results: any[]) => void
): void {
const results: any[] = [];
let currentIndex = 0;
function executeNext(): void {
if (currentIndex >= tasks.length) {
finalCallback(results);
return;
}
const task = tasks[currentIndex];
task((result) => {
results.push(result);
currentIndex++;
executeNext();
});
}
executeNext();
}
// Usage
executeSeries(
[
(callback) => {
setTimeout(() => {
console.log("Task 1");
callback("Result 1");
}, 1000);
},
(callback) => {
setTimeout(() => {
console.log("Task 2");
callback("Result 2");
}, 500);
},
(callback) => {
setTimeout(() => {
console.log("Task 3");
callback("Result 3");
}, 200);
}
],
(results) => {
console.log("All tasks complete:", results);
}
);
10. Custom Sort with Callback
- Implement
sortArrayfunction - Use callback to compare elements
- Return sorted array
💡 Show Answer
function sortArray<T>(
arr: T[],
compareFn: (a: T, b: T) => number
): T[] {
const result = [...arr]; // Create copy
// Bubble sort implementation
for (let i = 0; i < result.length; i++) {
for (let j = 0; j < result.length - i - 1; j++) {
if (compareFn(result[j], result[j + 1]) > 0) {
// Swap
[result[j], result[j + 1]] = [result[j + 1], result[j]];
}
}
}
return result;
}
// Sort numbers ascending
const numbers = [5, 2, 8, 1, 9];
const ascending = sortArray(numbers, (a, b) => a - b);
console.log(ascending); // [1, 2, 5, 8, 9]
// Sort numbers descending
const descending = sortArray(numbers, (a, b) => b - a);
console.log(descending); // [9, 8, 5, 2, 1]
// Sort strings
const fruits = ["banana", "apple", "cherry"];
const sortedFruits = sortArray(fruits, (a, b) => a.localeCompare(b));
console.log(sortedFruits); // ["apple", "banana", "cherry"]
// Sort objects
interface Person {
name: string;
age: number;
}
const people: Person[] = [
{ name: "Charlie", age: 35 },
{ name: "Alice", age: 25 },
{ name: "Bob", age: 30 }
];
const byAge = sortArray(people, (a, b) => a.age - b.age);
console.log(byAge);
// [{ name: "Alice", age: 25 }, { name: "Bob", age: 30 }, { name: "Charlie", age: 35 }]
const byName = sortArray(people, (a, b) => a.name.localeCompare(b.name));
console.log(byName);
// [{ name: "Alice", age: 25 }, { name: "Bob", age: 30 }, { name: "Charlie", age: 35 }]
Summary
In this chapter, you learned:
✅ What callbacks are and why they're useful
✅ How to define and use callbacks with TypeScript types
✅ Synchronous vs asynchronous callbacks
✅ Error-first callback pattern
✅ Callback types and interfaces
✅ Common use cases (arrays, events, timers)
✅ Implementing higher-order functions
Key Takeaways:
- Callbacks are functions passed as arguments to other functions
- TypeScript adds type safety to callbacks
- Callbacks enable flexible, reusable code
- Used extensively for async operations
- Foundation for understanding Promises and async/await
- Higher-order functions use callbacks to transform data
Common Patterns:
- Array methods:
map(),filter(),reduce(),forEach() - Event handlers:
addEventListener() - Timers:
setTimeout(),setInterval() - Error-first callbacks:
(error, result) => {} - Node.js style: callbacks as last parameter
Limitations:
- Can lead to "callback hell" with nested callbacks
- Error handling can be complex
- Harder to reason about flow with many callbacks
- Promises and async/await solve many of these issues
In the next chapter, we'll learn about Promises & Async/Await which provide a cleaner way to handle asynchronous operations!
Continue to Chapter 12: Promises & Async/Await →