My Hands-On Take on TypeScript’s forEach Loop

I’m Kayla. I build apps in TypeScript most days, and I’m not shy about what helps or what hurts. The array forEach loop? I’ve used it a lot—shipping features, fixing bugs, even tidying reports before lunch. It’s quick. It’s neat. And sometimes, it bites. If you’d like an even deeper dive into every corner case I’ve found, take a peek at my hands-on take on TypeScript’s forEach loop over on Improving Code. Need a refresher on the official behavior? The TypeScript forEach glossary entry lays out the mechanics in plain terms.

You know what? I still reach for it. But I’ve learned where it shines and where it falls flat. Let me show you what I mean with real code from my week. For an alternate angle, I also put together my honest take on the TypeScript forEach loop that digs into trade-offs I hit in production.

The gist, in plain words

forEach runs a function on each item in a list. It’s simple. You give it a callback, and it walks the array, left to right.

  • It’s great for side effects: logging, counters, DOM tweaks.
  • It does not return a new array.
  • It does not let you stop early.
  • And it doesn’t play nice with await.

That last one matters more than folks think.

What I like (and why I keep it in my pocket)

  • Clean and readable. Fewer braces, fewer vars.
  • TypeScript infers types well, so I write less noise.
  • Index and item are both there when I need them.
  • Map and Set have it too, so it feels familiar.

When I’m skimming code in VS Code at 7 a.m., this matters. I can “see” the flow faster. Prettier cleans it up. ESLint leaves it alone. Life’s good.

Real examples from my week

Here are some tiny slices from actual tasks I did.

1) Sum a cart (simple math, no drama)

type CartItem = { name: string; price: number; qty: number };

const cart: CartItem[] = [
  { name: 'Tea', price: 4, qty: 2 },
  { name: 'Honey', price: 8, qty: 1 },
];

let total = 0;

cart.forEach(item => {
  total += item.price * item.qty;
});

console.log(total); // 16

I could use reduce, sure. But this reads easy when I’m in a hurry.

2) Show tags with the index (tiny UI helper)

const tags: string[] = ['alpha', 'beta', 'prod'];

tags.forEach((tag, i) => {
  console.log(`${i}: ${tag}`);
});
// 0: alpha
// 1: beta
// 2: prod

Fast way to label rows or build a numbered list.

3) Narrow types while looping (yes, this works)

const mixed: Array<string | number> = [1, 'apple', 2, 'pear'];
const numbers: number[] = [];

mixed.forEach(x => {
  if (typeof x === 'number') {
    numbers.push(x);
  }
});

console.log(numbers); // [1, 2]

TypeScript narrows inside the if. No fuss.

4) Map.forEach has a twist (value, then key)

This one tripped me once during a late fix.

const scores = new Map<string, number>([
  ['Mia', 9],
  ['Noah', 7],
]);

scores.forEach((value, key) => {
  console.log(`${key}: ${value}`);
});
// Mia: 9
// Noah: 7

For arrays it’s (item, index). For Map, it’s (value, key). The order flips. I wish it didn’t. But now I remember.

5) DOM tweaks in a mini admin page

const buttons = document.querySelectorAll<HTMLButtonElement>('button.save');

buttons.forEach(btn => {
  btn.disabled = true;
});

Quick, clear, safe. I used this before a bulk save. I hit a similar quick-win recently when accepting a file-path argument in a tiny CLI—see my hands-on take on handling file-path arguments in TypeScript for the pattern.

While we’re on the subject of iterating over collections to build slick user experiences, reading up on successful products can spark design ideas. A great example is this thorough Bumble review — it breaks down the app’s UX flow, matching logic, and monetization tactics, giving engineers concrete inspiration for how they might structure their own loops and state updates. Another UX case study worth scanning—especially if you’re curious how a niche, location-based marketplace keeps gallery pagination snappy—is the AdultLook El Centro listing where you can see a lean, image-first layout that still manages to keep requests light and performance respectable, offering fresh ideas for optimizing list rendering in your own SPA.

What trips me up (and how I fix it)

Here’s the big one: forEach and async don’t work the way you think. It won’t wait for your await. It just runs the callbacks and moves on. For a deeper technical explanation of why this happens, check out this detailed answer that walks through the pitfalls.

The “it prints done too soon” bug

const ids = [1, 2, 3];

const fetchUser = async (id: number) => {
  return new Promise<{ id: number }>(res =>
    setTimeout(() => res({ id }), 100)
  );
};

ids.forEach(async id => {
  const user = await fetchUser(id);
  console.log('user:', user);
});

console.log('done');
// "done" logs before the users finish

When I first hit this, I groaned. Then I changed the loop.

Two fixes that actually work

Use for...of inside an async function:

async function loadUsers() {
  for (const id of [1, 2, 3]) {
    const user = await fetchUser(id);
    console.log('user:', user);
  }
  console.log('done');
}

loadUsers();

Or run them in parallel with Promise.all:

async function loadUsersFast() {
  const users = await Promise.all([1, 2, 3].map(fetchUser));
  console.log(users);
  console.log('done');
}

loadUsersFast();

Both are clear. Both are safe. I pick one based on whether I need order.

You also can’t break

This is small but real. You can’t stop early with forEach.

const nums = [1, 2, 3, 4];
let found: number | undefined;

nums.forEach(n => {
  if (n === 3) {
    found = n; // can't break here
  }
});

Use for...of if you need to break:

for (const n of [1, 2, 3, 4]) {
  if (n === 3) {
    found = n;
    break;
  }
}

A niche thing: passing a thisArg

I don’t use this much, but it’s handy sometimes.

class Counter {
  count = 0;
  add(n: number) {
    this.count += n;
  }
}

const nums = [1, 2, 3];
const counter = new Counter();

nums.forEach(function (n) {
  this.add(n);
}, counter);

console.log(counter.count); // 6

That second argument sets this inside the callback. I still prefer arrow functions most days. If you’re more into making your APIs self-describing, you might like my experiment with TypeScript named arguments for constructors.

When I pick something else

  • I need a new array: I use map.
  • I need to filter items: I use filter.
  • I need to stop early: I use for...of or a plain for.
  • I need speed in a hot path: I reach for a for index loop. It’s a tiny bit faster in tight loops.

Small note: forEach returns void. If you’re building and returning data, it’s not the right tool.

Tiny tips that saved me time

  • Trust inference. Let TypeScript infer the item type most of the time.
  • Name things well. A good callback name beats a comment.
  • Keep side effects clear. Don’t mix logging with building arrays.
  • Map order warning. Remember (value, key)