You know what? I use TypeScript every day. And I reach for forEach a lot. It’s fast to write. It reads clean. But it’s not perfect. So here’s my plain, real review—what I love, what trips me up, and what I use instead when it fights me. I dug even deeper in my honest take on the TypeScript forEach loop if you want the blow-by-blow version.
What even is forEach?
forEach runs a function for every item in a list. In TypeScript, it keeps types tidy. It’s simple:
In case you need a quick refresher, here’s a succinct glossary entry for forEach that covers the signature.
- You can read the item.
- You can also read the index.
- You cannot stop early.
That last bit matters. A lot. For a deeper exploration of iteration techniques in TypeScript, check out this practical overview on Improving Code.
Real code from my week
1) Clean and collect numbers from strings
I had a list coming from a form. Some values had spaces. Some had junk. I just needed valid numbers.
const rawValues: string[] = [" 42 ", "hi", "100", " 7x", "0"];
const clean: number[] = [];
rawValues.forEach((val) => {
const num = Number(val.trim());
if (!Number.isNaN(num)) {
clean.push(num);
}
});
console.log(clean); // [42, 100, 0]
Simple. Clear. A tiny bit boring, which I like. That same pattern shows up when I’m slurping CSV rows into a database—something I covered in this Lambda + DynamoDB CSV import write-up.
2) Build a quick lookup map
I often build maps for fast reads. This one groups users by role.
type User = { id: string; name: string; role: "admin" | "staff" | "guest" };
const users: User[] = [
{ id: "1", name: "Ana", role: "admin" },
{ id: "2", name: "Bo", role: "staff" },
{ id: "3", name: "Cam", role: "staff" },
];
const byRole = new Map<User["role"], User[]>();
users.forEach((u) => {
if (!byRole.has(u.role)) byRole.set(u.role, []);
byRole.get(u.role)!.push(u);
});
// Quick check
console.log(byRole.get("staff")?.length); // 2
TypeScript keeps the role keys safe. No mystery strings sneaking in. As an aside, when you refactor a model and need to rename a field without losing its JSDoc, I’ve found a few tricks that help—documented in my hands-on guide to safely renaming a TypeScript field.
3) forEach on Map and Set
Yep, forEach works on other built-ins too.
const scores = new Map<string, number>([
["Ana", 9],
["Bo", 7],
["Cam", 10],
]);
scores.forEach((value, key) => {
console.log(`${key}: ${value}`);
});
const seen = new Set<string>();
["a", "b", "a"].forEach((v) => seen.add(v));
console.log(seen.has("a")); // true
Clear as day.
4) The async trap (I stepped in it, again)
I thought this would wait. It didn’t.
const ids = [1, 2, 3];
async function fetchUser(id: number) {
// pretend to fetch
return { id, name: `U${id}` };
}
const usersA: Array<{ id: number; name: string }> = [];
// This looks fine... but it's not
ids.forEach(async (id) => {
const u = await fetchUser(id);
usersA.push(u);
});
console.log(usersA); // Often []
forEach doesn’t wait for async work. It runs and moves on. My fix? Use for...of when I need await.
const usersB: Array<{ id: number; name: string }> = [];
for (const id of ids) {
const u = await fetchUser(id);
usersB.push(u);
}
console.log(usersB.length); // 3
When I’m racing all calls, I use Promise.all:
const usersC = await Promise.all(ids.map((id) => fetchUser(id)));
Handling asynchronous file processing is also where a solid file-path argument pattern in TypeScript can save you from a ton of boilerplate.
When it shines
- I want clean, readable loops.
- I don’t need to stop early.
- I’m pushing to a result list or map.
- I want types to guide me while I code.
Where it bites
- Can’t break early. No
break. Noreturnto exit the loop. - Async code doesn’t “wait.”
- Easy to hide side effects. You mutate stuff in place, sometimes by accident.
- Folks mix it up with
for...in. That one loops over keys, not items — a mistake the linter’sno-for-in-arrayrule loves to catch.
For a language-level comparison—why these pain points feel milder to me than similar ones in Python—check out my real-life take comparing TypeScript vs. Python.
A quick compare in plain talk
forEach: clean and simple; no early exit; beware async.for...of: great forawait, and you can break.for...in: keys on objects; not for arrays in most cases..map: returns a new array. Don’t use it for side effects..filter,.some,.every: sometimes better than a manual loop.
Here’s how I break early without forEach:
const nums = [1, 3, 5, 6, 7];
// Need the first even? Use for...of
let firstEven: number | undefined;
for (const n of nums) {
if (n % 2 === 0) {
firstEven = n;
break;
}
}
Or use .find:
const firstEven2 = nums.find((n) => n % 2 === 0);
TypeScript types feel nice here
Type hints inside forEach help me avoid silly bugs.
type Order = { id: string; total: number | null };
const orders: Order[] = [
{ id: "a1", total: 12 },
{ id: "b2", total: null },
];
let sum = 0;
orders.forEach((o) => {
if (o.total !== null) {
sum += o.total; // TS knows total is number here
}
});
console.log(sum); // 12
That little null check saves me from crashes. I like that a lot. Keeping those little null-checks clear is exactly why I sprinkle thoughtful inline notes; see my real-world approach to comments in TypeScript for some concrete patterns.
Small UI note (a quick detour)
I used this with DOM nodes, too. Works fine in TS when the types are set.
const buttons: NodeListOf<HTMLButtonElement> =
document.querySelectorAll("button.save");
buttons.forEach((btn, i) => {
btn.textContent = `Save ${i + 1}`;
});
Back to the main thing—forEach reads like a to-do list. That’s why I keep using it.
Tiny cheat sheet
// Basic
arr.forEach((item, index, array) => { /* work */ });
// Map
map.forEach((value, key) => { /* work */ });
// Set
set.forEach((value) => { /* work */ });
// Need to break? Use for...of
for (const item of arr) { if (stop) break; }
// Need async? Use for...of or Promise.all
for (const id of ids) { const x = await fetch(id); }
const all = await Promise.all(ids.map(fetch));
If you enjoy patterns that make call sites obvious, you might also like my experiment with named constructor arguments in TypeScript.
While we’re talking about things that are delightfully free—forEach comes right
