TypeScript has the same loops as JavaScript: for, for...of, for...in, while, do...while and the array method forEach. What it adds is a type for each loop variable. for...of gives the element type, for...in always gives string keys, and forEach gives the element type to its callback, which cannot break early.
| Loop | Over | Variable type |
|---|---|---|
| for (let i = 0; i < a.length; i++) | number[] | i: number |
| for (const x of a) | number[] | x: number |
| for (const k in obj) | { tea: number; cake: number } | k: string |
| for (const i in a) | number[] | i: string (not number) |
| for (const [k, v] of Object.entries(obj)) | { tea: number; cake: number } | k: string, v: number |
| for (const [k, v] of map) | Map<string, number> | k: string, v: number |
| a.forEach((x) => ...) | [string, number] tuple | x: string | number |
Updated
Pick a scenario to see the code, the type the compiler assigns to the loop variable, and what the loop prints when it runs. Two of them are compile errors on purpose, because those are the loops people most often get stuck on in TypeScript.
const scores = [90, 72, 85];
for (const s of scores) {
console.log(s * 2);
}Type: s: number
The element type of number[] flows straight into the loop variable.
Output
180
144
170Each request waits for the previous one. The result array is typed from the function's return type.
async function loadAll(ids: number[]): Promise<string[]> {
const names: string[] = [];
for (const id of ids) {
const res = await fetch(`/api/users/${id}`);
const user: { name: string } = await res.json();
names.push(user.name);
}
return names;
}map starts every request at once and Promise.all waits for all of them. The order of results matches the order of ids.
async function loadAllAtOnce(ids: number[]): Promise<string[]> {
return Promise.all(
ids.map(async (id) => {
const res = await fetch(`/api/users/${id}`);
const user: { name: string } = await res.json();
return user.name;
}),
);
}
TypeScript does not add any loop syntax. It compiles every loop on this page to the same JavaScript, and the JavaScript loops guide on this site covers what each one does at runtime. Here the focus is the compiler: what type each loop variable gets, and the errors that follow.
Most of the time inference does the work. You rarely annotate a loop variable, and if you feel the need to, it usually means the collection itself needs a better type.
Objects are the awkward case. for...in and Object.keys both hand you plain strings, and indexing a typed object with a plain string is an error under strict mode. Object.entries avoids that by returning [string, value] pairs, and its value type comes from the object's values.
If the object is a lookup table you loop over often, consider typing it as Record<string, V> or storing it in a Map. Both make the loop variable's type obvious and the error disappears.
A loop over a union array, such as (string | number)[], gives a union variable. Narrow it with typeof inside the body, and the compiler tracks the narrower type for the rest of that branch.
Narrowing does not survive across rounds of the loop or into callbacks that run later, so check again where the value is used.
for...in visits enumerable string keys, including ones inherited through the prototype chain, and TypeScript cannot rule out extra keys on an object at runtime. Typing the key as keyof T would be a promise the language cannot keep, so it stays string.
Switch to Object.entries(obj) to get keys and values together, or cast the keys once: for (const k of Object.keys(obj) as (keyof typeof obj)[]). Only cast when you know the object has no extra keys.
No. As in JavaScript, forEach stops only if the callback throws. Use for...of with break, or some() and find(), which stop at the first match.
Yes, with a target of ES2015 or later. Each round gives a [key, value] pair typed from the Map's generics. For older targets, turn on downlevelIteration.
let i = 0 infers number, and a[i] gives the element type. With noUncheckedIndexedAccess on, a[i] becomes the element type or undefined, which is stricter but closer to the truth.
Related: JavaScript loops compared, TypeScript Map, TypeScript Record type, TypeScript data types, TypeScript vs JavaScript.
Start for free or see all free tools. We.Inc is free to start, with no card needed. AI building is on paid plans from $20 a month.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.