Polyglot
Languages TypeScript loops
TypeScript § loops

Loops

TypeScript inherits JavaScript’s iteration surface: the C-style for, the while, the do-while, and the iterator-based for...of and for...in. The principal contemporary forms are for...of (over iterables) and the array methods (forEach, map, filter, reduce). The combination — explicit for for index-based loops, for...of for iterables, array methods for transformations — covers the routine iteration surface; the conventional discipline favours array methods for substantial transformations and explicit loops for control-heavy iteration.

The C-style for

for (let i = 0; i < 10; i++) {
    console.log(i);
}

The form: for (init; cond; update) { body }. Each part is optional but the semicolons are required.

// Forever:
for (;;) {
    if (done) break;
}

// Reverse:
for (let i = arr.length - 1; i >= 0; i--) {
    console.log(arr[i]);
}

// Step by 2:
for (let i = 0; i < 100; i += 2) {
    console.log(i);
}

// Multiple variables:
for (let i = 0, j = arr.length - 1; i < j; i++, j--) {
    [arr[i], arr[j]] = [arr[j], arr[i]];          // swap
}

The conventional discipline uses the C-style form when explicit indexed access is needed; otherwise for...of or array methods are conventional.

for...of

The conventional iteration over iterables:

const arr = [10, 20, 30];

for (const x of arr) {
    console.log(x);                               // 10, 20, 30
}

// Strings:
for (const c of "hello") {
    console.log(c);                               // "h", "e", "l", "l", "o"
}

// Maps and Sets:
const set = new Set([1, 2, 3]);
for (const x of set) {
    console.log(x);
}

const map = new Map([["a", 1], ["b", 2]]);
for (const [key, value] of map) {
    console.log(key, value);
}

The form admits iterating any iterable — a value with a [Symbol.iterator]() method.

For index and value, entries:

for (const [i, x] of arr.entries()) {
    console.log(`[${i}] = ${x}`);
}

for...in

Iterates over the enumerable property keys of an object — including inherited keys:

const obj = { a: 1, b: 2, c: 3 };

for (const key in obj) {
    console.log(key, obj[key]);
}

A common pitfall: for...in on arrays iterates over indexes as strings, including any added properties:

const arr = [10, 20, 30];
arr.foo = "bar";                                  // (in JS) admits adding properties

for (const i in arr) {
    console.log(i);                               // "0", "1", "2", "foo"
}

The conventional discipline avoids for...in for arrays; for...of is the conventional choice. For object keys, Object.keys(obj) plus for...of is conventionally clearer than for...in:

for (const key of Object.keys(obj)) {
    console.log(key, obj[key]);
}

while and do...while

let n = 10;
while (n > 0) {
    console.log(n);
    n--;
}

let m = 0;
do {
    console.log(m);
    m++;
} while (m < 3);

The do...while runs the body at least once, then checks the condition. Conventional uses are polling and read-then-test patterns; the while-with-break form is conventional in TypeScript.

break and continue

for (let i = 0; i < 100; i++) {
    if (i === 50) break;                         // exit
    if (i % 2 === 0) continue;                   // skip
    console.log(i);
}

The break exits the innermost loop; continue proceeds to the next iteration. For nested loops, labels admit targeting:

outer: for (let i = 0; i < 10; i++) {
    for (let j = 0; j < 10; j++) {
        if (i * j > 50) break outer;
    }
}

The label form is rarely used in idiomatic code; restructuring into a function with return is conventionally clearer.

Array methods

The conventional contemporary discipline favours array methods for transformations:

forEach

arr.forEach(x => console.log(x));
arr.forEach((x, i) => console.log(`[${i}] = ${x}`));

The forEach is not a transformation — it returns undefined. For genuine iteration with side effects, conventional; for value-producing transformations, the other methods are conventional.

A subtle limitation: forEach does not support early termination via break or return. The conventional alternatives are for...of or some / every:

// Doesn't break out of forEach:
arr.forEach(x => {
    if (x > 100) return;                          // only "returns" from this callback
});

// Conventional alternative:
for (const x of arr) {
    if (x > 100) break;                          // truly breaks
}

map

const doubled = arr.map(x => x * 2);
const lengths = strings.map(s => s.length);
const indexed = arr.map((x, i) => ({ index: i, value: x }));

The map produces a new array of the same length with each element transformed.

filter

const evens = arr.filter(x => x % 2 === 0);
const positives = nums.filter(x => x > 0);

The filter produces a new array containing only elements for which the predicate returns truthy.

reduce

const sum = arr.reduce((acc, x) => acc + x, 0);
const max = arr.reduce((m, x) => x > m ? x : m, -Infinity);

// Build an object:
const counts = words.reduce<Record<string, number>>((acc, w) => {
    acc[w] = (acc[w] ?? 0) + 1;
    return acc;
}, {});

The reduce accumulates over the array; the second argument is the initial value.

For reduceRight, the iteration is from end to start.

find, findIndex, findLast

const first = arr.find(x => x > 10);              // first matching element (or undefined)
const idx = arr.findIndex(x => x > 10);           // index (or -1)
const last = arr.findLast(x => x < 10);           // last matching (ES2023)

some and every

const hasEven = arr.some(x => x % 2 === 0);
const allPositive = arr.every(x => x > 0);

The some short-circuits on the first true; every short-circuits on the first false.

flat and flatMap

const flat = [[1, 2], [3, 4]].flat();             // [1, 2, 3, 4]
const deep = [[[1]], [[2]]].flat(2);              // [1, 2]

const result = arr.flatMap(x => [x, x * 2]);
// Equivalent to: arr.map(x => [x, x * 2]).flat()

includes, indexOf

arr.includes(10);                                 // true / false
arr.indexOf(10);                                  // index or -1
arr.lastIndexOf(10);

Sorting

const sorted = [...arr].sort();                  // mutates a copy
arr.sort();                                       // mutates in place
arr.sort((a, b) => a - b);                       // ascending
arr.sort((a, b) => b - a);                       // descending

// Strings:
arr.sort((a, b) => a.localeCompare(b));

// By property:
people.sort((a, b) => a.age - b.age);

// Stable in modern engines (since ES2019):
people.sort((a, b) => a.age - b.age);

Method chaining

The conventional functional-style transformation:

const result = items
    .filter(x => x.active)
    .map(x => ({ id: x.id, name: x.name }))
    .sort((a, b) => a.name.localeCompare(b.name));

The form is conventional for substantial pipelines.

Iteration over Map, Set, and other iterables

const map = new Map([["a", 1], ["b", 2]]);

for (const [key, value] of map) { /* ... */ }
for (const key of map.keys()) { /* ... */ }
for (const value of map.values()) { /* ... */ }
for (const entry of map.entries()) { /* ... */ }

const set = new Set([1, 2, 3]);

for (const x of set) { /* ... */ }

For Map, the iteration order is insertion order — predictable and stable.

for await...of

For async iterables:

async function readLines(stream: AsyncIterable<string>) {
    for await (const line of stream) {
        console.log(line);
    }
}

Treated in Async and concurrency.

Common patterns

Iterate with index

for (const [i, x] of arr.entries()) {
    console.log(`[${i}] = ${x}`);
}

// Or:
arr.forEach((x, i) => console.log(`[${i}] = ${x}`));

Range iteration

JavaScript has no built-in range; common patterns:

// Standard for loop:
for (let i = 0; i < 10; i++) { /* ... */ }

// Array.from with mapper:
const range = Array.from({ length: 10 }, (_, i) => i);          // [0, 1, ..., 9]
for (const i of range) { /* ... */ }

// Array spread:
const range2 = [...Array(10).keys()];

// Generator (more elaborate):
function* range(n: number) { for (let i = 0; i < n; i++) yield i; }
for (const i of range(10)) { /* ... */ }

Filter and map combined

const result = items
    .filter(x => x.active)
    .map(x => x.name);

Counting

const counts = words.reduce<Record<string, number>>((acc, w) => {
    acc[w] = (acc[w] ?? 0) + 1;
    return acc;
}, {});

// Or with Map:
const counts = new Map<string, number>();
for (const w of words) {
    counts.set(w, (counts.get(w) ?? 0) + 1);
}

Group by

const byCategory = items.reduce<Record<string, Item[]>>((acc, item) => {
    (acc[item.category] ??= []).push(item);
    return acc;
}, {});

// ES2024:
// const byCategory = Object.groupBy(items, item => item.category);

The Object.groupBy and Map.groupBy methods (ES2024) admit substantial conciseness.

Finding

const first = items.find(x => x.id === target);
const idx = items.findIndex(x => x.id === target);
const all = items.filter(x => x.tag === "important");

Sum, max, min

const sum = nums.reduce((a, b) => a + b, 0);
const max = nums.reduce((a, b) => (a > b ? a : b), -Infinity);
const min = Math.min(...nums);                   // works for short arrays
const max2 = Math.max(...nums);

For substantial arrays, the spread of Math.max may exceed argument limits; the reduce form is the conventional defence.

Iterating object entries

const obj = { a: 1, b: 2, c: 3 };

for (const [key, value] of Object.entries(obj)) {
    console.log(`${key} = ${value}`);
}

// Or with Object.keys + lookup:
for (const key of Object.keys(obj)) {
    console.log(`${key} = ${obj[key as keyof typeof obj]}`);
}

Generators

The function* syntax produces a generator — an iterator-producing function:

function* fibonacci(): Generator<number> {
    let [a, b] = [0, 1];
    while (true) {
        yield a;
        [a, b] = [b, a + b];
    }
}

const gen = fibonacci();
for (const f of gen) {
    if (f > 100) break;
    console.log(f);                               // 0, 1, 1, 2, 3, 5, 8, ...
}

// Take a finite slice:
const fibs = [];
const it = fibonacci();
for (let i = 0; i < 10; i++) {
    fibs.push(it.next().value);
}

Generators admit substantial flexibility for lazy iteration and infinite sequences.

Custom iterables

class Range {
    constructor(public start: number, public end: number) {}

    *[Symbol.iterator](): Generator<number> {
        for (let i = this.start; i < this.end; i++) {
            yield i;
        }
    }
}

for (const x of new Range(0, 5)) {
    console.log(x);                               // 0, 1, 2, 3, 4
}

const arr = [...new Range(0, 5)];                 // [0, 1, 2, 3, 4]

The [Symbol.iterator] admits making any class iterable.

Iteration with break-equivalent

const result = items.find(x => x.matches);       // returns first match or undefined
if (result) { /* ... */ }

// Or:
const result = items.some(x => {
    if (x.matches) {
        process(x);
        return true;                              // breaks
    }
    return false;
});

Iterating Map entries

const map = new Map<string, number>();
// ... populate ...

for (const [key, value] of map.entries()) {
    console.log(`${key} = ${value}`);
}

// Iterate sorted by key:
const sortedKeys = [...map.keys()].sort();
for (const key of sortedKeys) {
    console.log(`${key} = ${map.get(key)}`);
}

A note on the conventional discipline

The contemporary TypeScript loops advice:

  • Use for...of for iterating arrays, Maps, Sets, strings, and custom iterables.
  • Use array methods (map, filter, reduce, forEach) for transformations.
  • Use for (C-style) for index-based iteration.
  • Avoid for...in for arrays; use for...of or Object.keys.
  • Avoid var in loop initialisers; use let.
  • Use generators for lazy iteration.
  • Use the entries(), keys(), values() methods for granular iteration.
  • Use for await...of for async iterables.
  • Chain array methods for substantial transformations.

The combination — multiple loop forms (for, while, do, for...of, for...in), array methods for functional transformations, generators for lazy iteration, async iterables — is the substance of TypeScript’s iteration surface. The discipline produces clear, expressive iteration code; the array-method style admits substantial conciseness for routine transformations.