Skip to main content

Using forEach to Work with Arrays in TypeScript

The forEach method provides a clean way to iterate through arrays in TypeScript. It simplifies array operations by removing the need for index management and loop counters found in traditional for loops. This built-in array method adds type safety and better IDE support to array iteration..

const numbers = [1, 2, 3, 4];
numbers.forEach(num => console.log(num)); // Logs each number in the array

Syntax and Basic Usage of forEach

The forEach method takes a callback function that executes on each array element. This function can accept three parameters: the current element, index, and the entire array as arguments.

const fruits = ['apple', 'banana', 'cherry'];
// Basic usage with just the element
fruits.forEach((fruit, index) => console.log(`${index}: ${fruit}`));
// Using index and element
fruits.forEach((fruit, index) => console.log(`${index}: ${fruit}`));
// Type-safe callback parameters
type Fruit = string;
const typedFruits: Fruit[] = ['apple', 'banana', 'cherry'];
typedFruits.forEach((fruit: Fruit) => console.log(fruit));

Handling Errors in a forEach Loop

Since forEach doesn't support breaking out of the loop or skipping iterations, you'll need a specific strategy for error handling. Here's how to handle errors effectively:

type UserData = {
id: string;
settings: { theme: string }
}

const users: UserData[] = [
{ id: 'user1', settings: { theme: 'dark' }},
{ id: 'user2', settings: { theme: 'light' }},
];

// Handle errors for each iteration independently
users.forEach(user => {
try {
processUserSettings(user.settings);
} catch (error) {
// Log error but continue processing other users
console.error(`Failed to process settings for ${user.id}:`, error);
}
});

// With async operations
const processUsers = async () => {
const errorLog: string[] = [];

await Promise.all(users.map(async user => {
try {
await updateUserSettings(user);
} catch (error) {
errorLog.push(`${user.id}: ${error.message}`);
}
}));

return errorLog;
};

For managing error states in your application, check out our guide on state management without useState. When working with multiple async operations, our functional relationships helpers article shows how to handle complex data dependencies. Note that for asynchronous operations, TypeScript map with Promise.all() often works better than forEach. This pattern lets you track all errors while still processing the entire array.

Stopping a forEach Loop Early

Unlike other loops, forEach doesn't let you break out early. However, you can use methods like some() or every() as alternatives.

const numbers = [1, 2, -3, 4, 5];
numbers.some((number) => {
if (number < 0) return true;
console.log(number);
return false;
});

Using async/await with forEach

TypeScript's type system helps catch common async operation mistakes, but forEach doesn't wait for promises to resolve. For async operations, you'll typically want other approaches:

// Not recommended - promises execute in parallel without waiting
urls.forEach(async (url) => {
const response = await fetch(url);
const data = await response.json();
console.log(data);
});

// Better approach - using for...of
async function fetchData() {
for (const url of urls) {
const response = await fetch(url);
const data = await response.json();
console.log(data);
}
}

// Parallel execution with Promise.all and map
async function fetchAllData() {
const responses = await Promise.all(
urls.map(async url => {
const response = await fetch(url);
return response.json();
})
);
console.log(responses);

For handling async data in Convex applications, check out our guide on complex filters which demonstrates proper async data fetching patterns. When working with arrays of promises, map often provides better type inference than forEach.

Passing a Named Function to forEach

You can use a named function as a callback in forEach for better readability and maintenance. TypeScript's type system helps ensure your callback functions match the array's type:

interface LogEntry {
timestamp: Date;
message: string;
level: 'info' | 'warn' | 'error';
}

const logs: LogEntry[] = [
{ timestamp: new Date(), message: 'Server started', level: 'info' },
{ timestamp: new Date(), message: 'Connection failed', level: 'error' }
];

function processLogEntry(entry: LogEntry, index: number) {
const formattedTime = entry.timestamp.toISOString();
console.log(`[${index}] ${formattedTime} - ${entry.level}: ${entry.message}`);
}

// TypeScript verifies the callback signature matches LogEntry type
logs.forEach(processLogEntry);

When building data processing pipelines, consider using reduce for accumulating results or filter to select specific entries. For real-time data handling in Convex, the functional relationships guide shows how to structure your data transformations.

Using forEach with Maps and Sets

Maps and Sets in TypeScript come with their own specialized forEach implementations, each with correctly typed callbacks.

Iterating Over a Map's Entries

// Type-safe Map declaration
const userPreferences = new Map<string, { theme: string; notifications: boolean }>([
['alice', { theme: 'dark', notifications: true }],
['bob', { theme: 'light', notifications: false }]
]);

userPreferences.forEach((preferences, username) => {
console.log(`${username}'s theme: ${preferences.theme}`);
});

Iterating Over a Set's Values

// Set with union type
const validStatuses = new Set<'pending' | 'active' | 'completed'>([
'pending', 'active', 'completed'
]);

validStatuses.forEach(status => {
validateStatus(status); // TypeScript ensures status is type-safe
});

These collection types work seamlessly with Convex's state management patterns for tracking unique values and key-value relationships. For array transformations, map and filter provide alternative ways to process your data.

Choosing Between forEach and Other Array Methods

forEach vs for vs for...of

Each loop type has specific use cases. Knowing when to use forEach, for, or for...of is important for effective array handling.

interface Task {
id: number;
title: string;
completed: boolean;
}

const tasks: Task[] = [
{ id: 1, title: 'Review code', completed: false },
{ id: 2, title: 'Update docs', completed: true }
];

// for loop when you need to access/compare adjacent elements
for (let i = 0; i < tasks.length - 1; i++) {
if (tasks[i].completed && !tasks[i + 1].completed) {
notifyIncomplete(tasks[i + 1]);
}
}

// forEach for side effects (logging, DOM updates)
tasks.forEach(task => {
updateTaskUI(task);
});

// for...of when you need to use 'await' or 'break'
for (const task of tasks) {
if (task.completed) break;
await processTask(task);
}

// map when transforming data
const taskTitles = tasks.map(task => task.title);

When to Use forEach

forEach shines when you need to perform side effects on array elements, like updating UI components or sending analytics events. For data transformations, consider map or filter. For async operations, choose for for...of loops. Check out our TypeScript guides for more array handling patterns in full-stack applications.