Skip to main content

How to Use TypeScript for Loops

You've got an array of API responses that need validation, or maybe you're processing user records to update their status. You watn to use a loop, but which one? TypeScript gives you several options: the classic for loop, for...of, for...in, and even forEach(). Each has its place, and picking the wrong one can lead to bugs or poor performance.

In this guide, we'll cover the different TypeScript loop patterns and when to use each one. You'll learn how to iterate efficiently, avoid common pitfalls, and write cleaner code that handles real-world scenarios.

Choosing the Right Loop Type

TypeScript offers several loop types, and picking the right one makes your code clearer and more efficient. Here's when to use each:

Loop TypeBest ForExample Use Case
for (traditional)When you need the index or precise control over iterationIterating backwards, skipping elements, modifying based on position
for...ofIterating over array values or other iterablesProcessing each element when you don't need the index
for...inIterating over object keys (properties)Reading or transforming object properties
forEach()Simple array iteration with side effectsLogging, updating external state (but can't break early)
const productPrices = [29.99, 49.99, 19.99, 99.99];

// Traditional for: when you need the index
for (let i = 0; i < productPrices.length; i++) {
console.log(`Product ${i + 1}: $${productPrices[i]}`);
}

// for...of: when you just need the values
for (const price of productPrices) {
console.log(`Price: $${price}`);
}

// for...in: for object properties (not recommended for arrays)
const config = { apiUrl: 'https://api.example.com', timeout: 5000 };
for (const key in config) {
console.log(`${key}: ${config[key]}`);
}

The for...of loop is your go-to for arrays when you don't need the index. It's cleaner and less error-prone than traditional for loops. Save for...in for objects only, as using it on arrays can lead to unexpected behavior with inherited properties.

Iterating over Arrays with a For Loop

Arrays are probably what you'll loop through most often. You've got two main options: the traditional indexed for loop or the cleaner for...of loop.

// Traditional for loop with index
const numbers = [1, 2, 3, 4, 5];
for (let i = 0; i < numbers.length; i++) {
console.log(numbers[i]);
}

// Cleaner for...of loop
for (const number of numbers) {
console.log(number);
}

The indexed loop gives you access to array positions, which is useful when you need to know where you are in the array. The for...of loop is cleaner when you just need the values. Pick based on whether the index matters for your use case.

Looping Through Objects

Working with object types in TypeScript? You've got several ways to iterate over their properties, depending on what you need:

// Loop over keys with for...in
const person = { name: 'John', age: 30 };
for (const key in person) {
console.log(key); // 'name', 'age'
}

// Loop over values with Object.values()
for (const value of Object.values(person)) {
console.log(value); // 'John', 30
}

// Get both keys and values with Object.entries()
for (const [key, value] of Object.entries(person)) {
console.log(`${key}: ${value}`); // 'name: John', 'age: 30'
}

Use for...in when you only care about property names. Reach for Object.values() when you just need the values. And grab Object.entries() when you need both the key and value together (which is pretty common).

Getting Both Index and Value

Sometimes you need both the array element and its position. Maybe you're building a numbered list or need to reference neighboring elements. You can do this with a traditional for loop or the entries() method:

// Traditional approach with index
const colors = ['red', 'green', 'blue'];
for (let i = 0; i < colors.length; i++) {
console.log(`Color at index ${i}: ${colors[i]}`);
}

// Cleaner approach with entries()
for (const [index, color] of colors.entries()) {
console.log(`Color at index ${index}: ${color}`);
}

The traditional index loop gives you more control (you can easily iterate backwards or skip elements), while entries() is cleaner when you just need to process each item with its index. For performance-critical Convex code that processes large arrays, check out the tips in Convex's TypeScript best practices guide.

Looping Through Sets and Maps

Beyond arrays and objects, TypeScript lets you loop through Sets and Map<K, V> collections using for...of. Both are iterable by default:

// Loop through a Set
const uniqueNumbers = new Set([1, 2, 3, 4, 5]);
for (const number of uniqueNumbers) {
console.log(number);
}

// Loop through a Map
const config = new Map([
['apiUrl', 'https://api.example.com'],
['timeout', 5000],
]);
for (const [key, value] of config) {
console.log(`${key}: ${value}`);
}

Notice how Maps automatically destructure into [key, value] pairs, similar to how Object.entries() works. This consistent pattern makes it natural to work with different collection types. The same patterns work with Convex database's data structures, so you can apply these techniques to both local and database collections.

Combining Loops with Conditionals

Loops and conditionals work great together when you need to filter or process data differently based on its value:

// Process elements differently based on conditions
const numbers = [1, 2, 3, 4, 5];
for (const number of numbers) {
if (number % 2 === 0) {
console.log(`Even number: ${number}`);
} else {
console.log(`Odd number: ${number}`);
}
}

// Only process items that meet criteria
const products = [
{ name: 'Laptop', price: 999, inStock: true },
{ name: 'Mouse', price: 29, inStock: false },
{ name: 'Keyboard', price: 79, inStock: true },
];

for (const product of products) {
if (product.inStock && product.price < 500) {
console.log(`Available: ${product.name} at $${product.price}`);
}
}

Sure, array methods like filter() and map() can handle simple cases. But loops with conditionals give you more flexibility for complex logic and can be clearer when you're doing multiple operations. For examples of combining loops with filters in a database context, check out Convex's guide on complex filters.

Using break and continue Statements

Sometimes you need to exit a loop early or skip specific iterations. The break and continue statements give you that control, and they're particularly useful when processing large datasets or searching for specific values.

// Using break to exit when a condition is met
function findFirstExpensiveProduct(products: { name: string; price: number }[]) {
let expensiveProduct = null;

for (const product of products) {
if (product.price > 100) {
expensiveProduct = product;
break; // Stop searching once we find the first match
}
}

return expensiveProduct;
}

// Using continue to skip certain iterations
function processValidRecords(records: { id: number; isValid: boolean; data: string }[]) {
const results = [];

for (const record of records) {
if (!record.isValid) {
continue; // Skip invalid records
}

// Only process valid records
results.push(record.data.toUpperCase());
}

return results;
}

Here's an important limitation: you can't use break or continue with forEach(). That's because forEach() accepts a callback function, and these control statements only work within traditional loops. If you need to exit early or skip iterations, stick with for, for...of, or for...in loops.

// This will throw a syntax error
const numbers = [1, 2, 3, 4, 5];
numbers.forEach((num) => {
if (num === 3) {
break; // SyntaxError: Illegal break statement
}
});

// Use for...of instead
for (const num of numbers) {
if (num === 3) {
break; // This works fine
}
}

When working with nested loops, break only exits the innermost loop. If you need to break out of multiple levels, consider using a labeled statement or restructuring your code into a function where you can use return.

Handling Asynchronous Operations inside For Loops

When you're making API calls or database queries in a loop, you need to decide: should these operations run one after another (sequential), or all at once (parallel)? Each approach has trade-offs.

Sequential Processing with async/await: This processes items one at a time. Each iteration waits for the previous one to complete:

async function processUserUpdates(userIds: number[]) {
const results = [];

for (const id of userIds) {
// Each request waits for the previous one to finish
const response = await fetch(`https://api.example.com/users/${id}`);
const userData = await response.json();

// You can use the result immediately
console.log(`Processed user ${userData.name}`);
results.push(userData);
}

return results;
}

When to use sequential: Choose this when order matters, when you need the result of one operation before starting the next, or when you want to avoid overwhelming an API with simultaneous requests. It's also easier to debug and reason about.

Parallel Processing with Promise.all: This starts all operations at once and waits for all of them to complete:

async function processUserUpdatesParallel(userIds: number[]) {
// Start all fetches immediately
const promises = userIds.map((id) =>
fetch(`https://api.example.com/users/${id}`).then((res) => res.json())
);

// Wait for all to complete
const results = await Promise.all(promises);

return results;
}

When to use parallel: Choose this when operations are independent, when speed is critical, and when the server can handle concurrent requests. Processing 100 items sequentially might take 100 seconds (1 second each), while parallel processing could finish in just a few seconds.

The Hybrid Approach: Sometimes you need a middle ground. Process items in batches to balance speed and resource usage:

async function processBatches<T>(items: T[], batchSize: number, processFn: (item: T) => Promise<void>) {
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);

// Process this batch in parallel
await Promise.all(batch.map(processFn));

console.log(`Processed batch ${i / batchSize + 1}`);
}
}

// Usage: Process 1000 users in batches of 10
await processBatches(userIds, 10, async (id) => {
const response = await fetch(`https://api.example.com/users/${id}`);
const data = await response.json();
// Process the data
});

For database operations, you might want to check out Convex's TypeScript-first query patterns to handle data fetching efficiently. And remember: Promise<T> provides powerful tools for managing async operations beyond simple loops.

for...of vs forEach: When to Use Each

Both for...of loops and forEach() let you iterate over arrays, but they have key differences that affect which one you should choose.

const userIds = [101, 102, 103, 104, 105];

// Using for...of
for (const id of userIds) {
console.log(`Processing user ${id}`);
}

// Using forEach
userIds.forEach((id) => {
console.log(`Processing user ${id}`);
});

At first glance, they look similar. But here's where they differ:

Performance: The traditional for loop and for...of are typically faster than forEach() because forEach() involves a function call for each element. For small to medium arrays, the difference is negligible. But if you're processing tens of thousands of records, for...of will outperform forEach().

Control flow: This is the big one. With forEach(), you can't use break or continue to control iteration. Once you start a forEach(), it runs to completion. If you need to exit early when you find a specific item, use for...of.

Return values: If you try to return inside a forEach() callback, it only exits that callback, not the containing function. This catches developers off guard:

function findUser(users: { id: number; name: string }[], targetId: number) {
// This doesn't work as expected
users.forEach((user) => {
if (user.id === targetId) {
return user; // This only returns from the callback, not findUser!
}
});

// Use for...of instead
for (const user of users) {
if (user.id === targetId) {
return user; // This correctly returns from findUser
}
}
}

When to use forEach(): It shines when you're performing side effects (like logging, updating external state, or calling APIs) and you know you'll process every element. The functional style can make the code more readable for simple operations. For a deeper dive into forEach(), check out our TypeScript forEach guide.

When to use for...of: Choose this when you need control flow (break, continue, return), when performance matters, or when you're not sure if you'll need to exit early. It's also better for async operations where you want sequential processing.

Improving Performance with For Loops

When processing large datasets in TypeScript, choosing the right loop type and optimization strategy can make a real difference. Here's what you need to know:

Loop Performance Ranking: For performance-critical code, the traditional for loop is your fastest option, followed closely by for...of. Both significantly outperform forEach(), which can be 2-4 times slower on large arrays due to function call overhead. For most applications, this difference won't matter, but if you're processing hundreds of thousands of records, reach for for or for...of.

// Fastest: Traditional for loop
const products = new Array(100000).fill({ price: 29.99, discount: 0.1 });
for (let i = 0; i < products.length; i++) {
const finalPrice = products[i].price * (1 - products[i].discount);
// Process the result
}

// Nearly as fast: for...of
for (const product of products) {
const finalPrice = product.price * (1 - product.discount);
// Process the result
}

// Slower: forEach (but more readable for simple cases)
products.forEach((product) => {
const finalPrice = product.price * (1 - product.discount);
// Process the result
});

Exit Early to Save Cycles: When you're searching for a specific item, exiting as soon as you find it can save significant processing time:

function findExpiredSession(sessions: { id: string; expiresAt: number }[]) {
const now = Date.now();

for (const session of sessions) {
if (session.expiresAt < now) {
return session; // Stop immediately when found
}
}

return null;
}

Watch Out for Nested Loops: Nested loops can tank performance fast. A loop inside a loop creates exponential complexity. If you have 1,000 items in each array, that's 1,000,000 iterations:

// Be careful with nested loops
const users = getUserList(); // 1,000 users
const orders = getOrderList(); // 1,000 orders

// This creates 1,000,000 iterations!
for (const user of users) {
for (const order of orders) {
if (order.userId === user.id) {
// Match orders to users
}
}
}

// Better: Use a Map for lookups
const ordersByUser = new Map<string, Order[]>();
for (const order of orders) {
const userOrders = ordersByUser.get(order.userId) || [];
userOrders.push(order);
ordersByUser.set(order.userId, userOrders);
}

// Now just iterate once
for (const user of users) {
const userOrders = ordersByUser.get(user.id) || [];
// Process orders for this user
}

When working with databases, you can find more optimization strategies in Convex's performance best practices.

Final Thoughts on TypeScript For Loops

Mastering TypeScript loops comes down to picking the right tool for the job. Here's a quick reference for making that choice:

  • Use for...of as your default for arrays. It's clean, fast, and supports break and continue.
  • Use traditional for when you need the index or want to iterate in a specific pattern (backwards, every other item, etc.).
  • Use for...in only for object properties, never for arrays.
  • Use forEach() when you're sure you'll process every element and you like the functional style.
  • Use break to exit early and save processing time when you find what you're looking for.
  • Watch out for nested loops and consider using Maps or other data structures to avoid O(n²) complexity.
  • Think about sequential vs parallel when working with async operations in loops.

These patterns will serve you well whether you're validating API data, processing user records, or building complex data transformations. Want to experiment with these concepts? Try them out in the typescript playground.