Skip to main content

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 filterArray function
  • 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 mapArray function
  • 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 forEachArray function
  • 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 findInArray function
  • 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 reduceArray function
  • 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 sortArray function
  • 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 →