
Gli errori più comuni con forEach in TypeScript
forEach sembra il metodo più semplice degli array, ma in TypeScript nasconde trappole reali: callback async che TypeScript non segnala, narrowing perso nelle closure, indici impliciti any.
Array.prototype.forEach è probabilmente il metodo che si impara prima negli array JavaScript, e per questo viene usato anche dove non è la scelta giusta. In TypeScript, in particolare, ci sono almeno tre trappole che il type checker non segnala come ti aspetteresti — non per un bug del compilatore, ma per come sono progettate le sue regole.
1. Un callback async in forEach non dà errore (e dovrebbe preoccuparti)
Questo è il caso più insidioso perché compila senza avvisi:
async function processAll(items: string[]) {
items.forEach(async (item) => {
await saveToDatabase(item); // eseguito, ma nessuno aspetta questo
});
console.log("fatto"); // stampato PRIMA che i salvataggi finiscano
}
La firma di forEach si aspetta una callback che ritorna void. Una funzione async ritorna sempre una Promise, quindi tecnicamente il tipo di ritorno non è void — eppure TypeScript non protesta. Il motivo è una regola specifica del linguaggio: quando il tipo di ritorno atteso è void, TypeScript permette che la funzione fornita ritorni qualsiasi cosa, a patto che il valore venga ignorato. È una scelta di design pensata per casi come gli event handler (onClick={() => setCount(c => c + 1)} dove il valore di ritorno non conta), ma ha l’effetto collaterale di rendere invisibile un bug reale: forEach non aspetta le Promise, quindi le chiamate a saveToDatabase partono tutte più o meno insieme, senza controllo sull’ordine, e un eventuale rejection non viene catturato da nessun try/catch esterno.
La correzione: se serve await dentro il loop, forEach è lo strumento sbagliato. Usa for...of:
async function processAll(items: string[]) {
for (const item of items) {
await saveToDatabase(item);
}
console.log("fatto"); // ora è vero
}
Oppure, se vuoi eseguire in parallelo ma comunque aspettare il completamento:
await Promise.all(items.map((item) => saveToDatabase(item)));
La regola @typescript-eslint/no-misused-promises (disponibile anche via eslint-plugin-oxlint, se usi oxlint) esiste apposta per intercettare questo pattern, perché il compilatore da solo non lo fa.
2. return dentro forEach non è un break
Un errore concettuale comune per chi arriva da un linguaggio con cicli for classici:
function findFirstEven(numbers: number[]) {
numbers.forEach((n) => {
if (n % 2 === 0) {
return n; // NON esce dal forEach, solo dall'iterazione corrente
}
});
}
TypeScript non segnala nulla di sbagliato qui — sintatticamente è tutto corretto, semplicemente return dentro la callback si comporta come un continue, non come un break. Il valore ritornato viene scartato (coerentemente con il punto 1: forEach si aspetta void), e il ciclo continua fino all’ultimo elemento anche se hai già trovato quello che cercavi.
La correzione: se serve un’uscita anticipata, forEach non la supporta. Serve un for...of con break, oppure Array.prototype.find:
const firstEven = numbers.find((n) => n % 2 === 0);
3. Il narrowing si perde dentro la callback
Un caso meno noto ma reale, legato a come TypeScript ragiona sui confini di funzione:
function process(value: string | undefined, items: string[]) {
if (typeof value === "undefined") return;
items.forEach(() => {
console.log(value.toUpperCase()); // può dare errore qui
});
}
Se value non è marcato const (o se TypeScript non può garantire che non venga riassegnato prima che la callback venga eseguita — cosa che con una callback asincrona è impossibile escludere in generale), il narrowing fatto dall’if sopra non è garantito valere dentro una funzione annidata come la callback di forEach. Con strict: true — default da TypeScript 7, di cui ho parlato nell’articolo su TypeScript 7 — questi casi emergono più spesso di quanto capitasse con la configurazione permissiva di TypeScript 6.
La correzione: assegna il valore narrowed a una nuova variabile const prima di passarlo alla callback, così il tipo resta fissato:
function process(value: string | undefined, items: string[]) {
if (typeof value === "undefined") return;
const narrowedValue = value; // const: TypeScript ora può fidarsi
items.forEach(() => {
console.log(narrowedValue.toUpperCase());
});
}
La lezione comune ai tre casi
In tutti e tre gli esempi, TypeScript non sta segnalando un errore di sintassi o di tipo in senso stretto — sta semplicemente non proteggendoti da un errore logico che il sistema di tipi non è progettato per catturare. forEach è un’API più limitata di quanto sembri: niente uscita anticipata, niente gestione nativa di async, e closure che possono comportarsi diversamente da quanto ci si aspetta. Per la maggior parte dei casi in cui verrebbe naturale usarlo, for...of, map, find o Promise.all sono scelte più esplicite — e più difficili da usare male.

Co-Fondatore & CTO presso PAPION. Senior full-stack engineer specializzato in React, TypeScript, Node.js e sicurezza applicativa.