Loops #

Loops are the mechanism for executing a block of code repeatedly — whether with a known number of iterations, or while a certain condition holds. TypeScript inherits all loop constructs from JavaScript, but adds type safety that makes many classic bugs — like accessing the wrong index or calling a nonexistent method on an element — detectable at compile time rather than at runtime. What’s more, TypeScript strongly supports functional programming styles with array methods like map, filter, and reduce that are safer and more expressive than traditional imperative loops. This article covers all loop forms in TypeScript and when to use each appropriately.

for — Indexed Loops #

The classic for loop gives full control over initialization, condition, and per-iteration expressions. It’s most useful when you need index access, need to iterate backwards, or need to skip elements by a certain step.

// Standard forward loop
for (let i = 0; i < 5; i++) {
  console.log(`Iterasi ke-${i}`);
}

// Backward iteration — useful for removing elements from an array while iterating
const tugas: string[] = ["Belajar", "Kerja", "Olahraga", "Tidur"];
for (let i = tugas.length - 1; i >= 0; i--) {
  console.log(`${i + 1}. ${tugas[i]}`);
}

// Step of two per iteration
for (let i = 0; i <= 10; i += 2) {
  console.log(i); // 0, 2, 4, 6, 8, 10
}

Type Safety When Iterating Arrays #

TypeScript guarantees that the element type accessed via an index matches the array’s type:

const harga: number[] = [15_000, 25_000, 10_000, 45_000];

let total = 0;
for (let i = 0; i < harga.length; i++) {
  total += harga[i]; // harga[i] is of type number — safe
  // total += harga[i].toUpperCase(); // ✗ Error: 'toUpperCase' doesn't exist on 'number'
}
console.log(`Total: Rp ${total.toLocaleString("id-ID")}`);

// Array of objects — TypeScript knows the type of every property
interface Produk {
  nama: string;
  harga: number;
  stok: number;
}

const katalog: Produk[] = [
  { nama: "Kurma Ajwa", harga: 85_000, stok: 50 },
  { nama: "Madu Sidr", harga: 250_000, stok: 12 },
  { nama: "Minyak Zaitun", harga: 75_000, stok: 30 },
];

for (let i = 0; i < katalog.length; i++) {
  const produk = katalog[i]; // Type: Produk
  console.log(`${produk.nama}: Rp ${produk.harga.toLocaleString("id-ID")} (${produk.stok} tersisa)`);
}

The Out-of-Bounds Index Trap #

TypeScript with default configuration doesn’t detect array index access beyond bounds — the returned value is undefined, not a compile error. Enable noUncheckedIndexedAccess in tsconfig.json for more protection:

const angka: number[] = [1, 2, 3];

// Without noUncheckedIndexedAccess — TypeScript assumes this is always a number
const nilai = angka[10]; // Type: number — but its value is undefined at runtime!

// With noUncheckedIndexedAccess: true in tsconfig
// const nilai = angka[10]; // Type: number | undefined — more accurate and safe

while — Condition-Based Loops #

while executes a block of code as long as its condition is true. It’s best when the number of iterations isn’t known up front and depends on a condition that can change inside the loop.

// Simulating delivery with retry
async function kirimDenganRetry(
  data: string,
  maksPencobaan: number
): Promise<boolean> {
  let percobaan = 0;

  while (percobaan < maksPencobaan) {
    percobaan++;
    console.log(`Percobaan ${percobaan}/${maksPencobaan}...`);

    try {
      await kirimData(data); // Assume this async function exists
      console.log("Berhasil dikirim!");
      return true;
    } catch (err) {
      console.warn(`Gagal: ${(err as Error).message}`);
      if (percobaan < maksPencobaan) {
        await tunggu(1000 * percobaan); // Simple exponential backoff
      }
    }
  }

  console.error("Semua percobaan gagal");
  return false;
}

// Helper
function tunggu(ms: number): Promise<void> {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

The Infinite Loop Guard #

while is prone to infinite loops if the condition never becomes false. Always make sure something inside the loop will eventually turn the condition false:

// ANTI-PATTERN: The condition never changes — infinite loop
// let aktif = true;
// while (aktif) {
//   prosesData(); // If prosesData doesn't change 'aktif', the loop never stops
// }

// CORRECT: The condition definitely changes, or there's a break condition
let antrean: string[] = ["tugas-1", "tugas-2", "tugas-3"];
let batasWaktu = Date.now() + 5000; // 5-second limit

while (antrean.length > 0 && Date.now() < batasWaktu) {
  const tugas = antrean.shift()!; // Take from the front
  proseskanTugas(tugas);
}

if (antrean.length > 0) {
  console.warn(`${antrean.length} tugas belum selesai karena waktu habis`);
}

function proseskanTugas(tugas: string): void {
  console.log(`Memproses: ${tugas}`);
}

do...while — Executes at Least Once #

do...while guarantees the code block executes at least once before the condition is evaluated. Useful for scenarios like asking for user input or polling that must run at least once.

// Simulating an interactive menu that keeps appearing until the user exits
function tampilkanMenu(): string {
  // In a Node.js context, this could use readline
  // Here we simulate it with an array of choices
  const pilihan = ["1", "2", "3", "keluar"];
  return pilihan[Math.floor(Math.random() * pilihan.length)];
}

let pilihan: string;
do {
  pilihan = tampilkanMenu();
  console.log(`Pilihan: ${pilihan}`);

  switch (pilihan) {
    case "1": console.log("Membuka profil..."); break;
    case "2": console.log("Membuka pengaturan..."); break;
    case "3": console.log("Membuka bantuan..."); break;
  }
} while (pilihan !== "keluar");

console.log("Sampai jumpa!");

while vs do...while Comparison #

while:
  Check condition FIRST → execute if true
  Can execute 0 times if the condition is immediately false

do...while:
  Execute FIRST → check the condition afterwards
  At least 1 execution, whatever the condition

for...of — Value Iteration (The Primary Choice for Arrays) #

for...of is the most idiomatic and safe way to iterate array elements, strings, Sets, Maps, and other iterable objects in modern TypeScript. TypeScript infers the element type automatically:

const buah: string[] = ["mangga", "pisang", "jeruk", "apel"];

// TypeScript knows 'buah' is of type string on every iteration
for (const b of buah) {
  console.log(b.toUpperCase()); // ✓ .toUpperCase() is available because of the string type
}

// for...of on an array of objects — the full type is available
const karyawan: Array<{ nama: string; departemen: string; gaji: number }> = [
  { nama: "Budi", departemen: "Engineering", gaji: 15_000_000 },
  { nama: "Siti", departemen: "Design", gaji: 12_000_000 },
  { nama: "Ahmad", departemen: "Engineering", gaji: 18_000_000 },
];

for (const k of karyawan) {
  // k is of type { nama: string; departemen: string; gaji: number }
  console.log(`${k.nama} (${k.departemen}): Rp ${k.gaji.toLocaleString("id-ID")}`);
}

for...of with entries() — Values and Indices Together #

When you need the index and the value at once, use .entries():

const daftarTugas: string[] = ["Desain UI", "Implementasi API", "Testing", "Deploy"];

for (const [indeks, tugas] of daftarTugas.entries()) {
  // indeks: number, tugas: string — TypeScript infers both
  console.log(`${indeks + 1}. ${tugas}`);
}
// 1. Desain UI
// 2. Implementasi API
// 3. Testing
// 4. Deploy

for...of on Strings, Sets, and Maps #

// Iterating string characters
const teks = "TypeScript";
for (const karakter of teks) {
  process.stdout.write(karakter + " "); // T y p e S c r i p t
}

// Iterating a Set — every unique value
const tagUnik = new Set<string>(["typescript", "nodejs", "typescript", "golang"]);
for (const tag of tagUnik) {
  console.log(tag); // typescript, nodejs, golang (duplicates removed)
}

// Iterating a Map — every [key, value] pair
const konfigurasi = new Map<string, string>([
  ["host", "localhost"],
  ["port", "5432"],
  ["nama_db", "muslimapps"],
]);

for (const [kunci, nilai] of konfigurasi) {
  console.log(`${kunci} = ${nilai}`);
}

for...in — Object Key Iteration (Careful!) #

for...in iterates all enumerable properties of an object, including properties inherited from the prototype chain. This is rarely what you want for arrays.

// ANTI-PATTERN: Using for...in for arrays
const angka: number[] = [10, 20, 30];
for (const kunci in angka) {
  console.log(kunci); // "0", "1", "2" — keys are strings, not numbers!
  console.log(angka[kunci]); // This works but isn't type-safe
}
// If a library adds properties to Array.prototype,
// for...in will also iterate those extra properties — a subtle bug!

// CORRECT: for...of for arrays, for...in for plain objects
const pengaturan: Record<string, string | number> = {
  tema: "gelap",
  bahasa: "id",
  ukuranFont: 14,
};

for (const kunci in pengaturan) {
  // kunci is of type string
  if (Object.prototype.hasOwnProperty.call(pengaturan, kunci)) {
    // hasOwnProperty ensures we only iterate own properties,
    // not ones inherited from the prototype
    console.log(`${kunci}: ${pengaturan[kunci]}`);
  }
}
For arrays, always use for...of instead of for...in. for...in returns keys as string (not number), iterates properties inherited from the prototype, and its order isn’t guaranteed consistent across JavaScript engines. Use for...in only for plain objects, and always include a hasOwnProperty check.

Functional Array Methods — A More Expressive Alternative #

In modern TypeScript, many array loop cases can be replaced by built-in functional array methods that are more expressive, easier to chain, and easier to test. TypeScript provides full type inference on all these methods.

map — Element Transformation #

const hargaAsli: number[] = [100_000, 250_000, 75_000, 180_000];

// ANTI-PATTERN: A for loop for transformation
const hargaDiskon1: number[] = [];
for (const h of hargaAsli) {
  hargaDiskon1.push(h * 0.9);
}

// CORRECT: map — more concise, the return type is inferred automatically
const hargaDiskon2 = hargaAsli.map((h) => h * 0.9);
// Type: number[]

// Map with objects
interface Pengguna { id: number; nama: string; aktif: boolean }

const pengguna: Pengguna[] = [
  { id: 1, nama: "Budi", aktif: true },
  { id: 2, nama: "Siti", aktif: false },
  { id: 3, nama: "Ahmad", aktif: true },
];

// Transforming into a concise format for display
const ringkasan = pengguna.map(({ id, nama, aktif }) => ({
  label: `${id}: ${nama}`,
  warna: aktif ? "hijau" : "merah",
}));
// Type: Array<{ label: string; warna: string }>

filter — Filtering Elements #

const semuaProduk: Produk[] = [
  { nama: "Kurma", harga: 85_000, stok: 0 },
  { nama: "Madu", harga: 250_000, stok: 12 },
  { nama: "Minyak", harga: 75_000, stok: 30 },
  { nama: "Kismis", harga: 45_000, stok: 0 },
];

// Filter available products — the result type stays Produk[]
const produkTersedia = semuaProduk.filter((p) => p.stok > 0);
// [{ nama: "Madu", ... }, { nama: "Minyak", ... }]

// Filter with a type guard — narrows the result type
type NilaiMungkinNull = string | null | undefined;
const campuran: NilaiMungkinNull[] = ["satu", null, "dua", undefined, "tiga"];

// Without a type guard — the result is typed (string | null | undefined)[]
const tanpaGuard = campuran.filter((v) => v !== null && v !== undefined);

// With a type guard — the result is the more specific string[]
const denganGuard = campuran.filter((v): v is string => v !== null && v !== undefined);
// Type: string[] — TypeScript knows all null/undefined values are filtered out

reduce — Accumulating Values #

const transaksi: Array<{ jenis: "masuk" | "keluar"; jumlah: number }> = [
  { jenis: "masuk", jumlah: 5_000_000 },
  { jenis: "keluar", jumlah: 1_500_000 },
  { jenis: "masuk", jumlah: 2_000_000 },
  { jenis: "keluar", jumlah: 750_000 },
];

// Calculate the final balance
const saldo = transaksi.reduce((akumulasi, t) => {
  return t.jenis === "masuk"
    ? akumulasi + t.jumlah
    : akumulasi - t.jumlah;
}, 0); // Initial value: 0

console.log(`Saldo: Rp ${saldo.toLocaleString("id-ID")}`); // Rp 4.750.000

// Reduce for grouping data (group by)
const perDepartemen = karyawan.reduce(
  (kelompok, k) => {
    const dept = k.departemen;
    kelompok[dept] = kelompok[dept] ?? [];
    kelompok[dept].push(k.nama);
    return kelompok;
  },
  {} as Record<string, string[]>
);
// { Engineering: ["Budi", "Ahmad"], Design: ["Siti"] }

Chaining Array Methods #

Array methods can be chained together to build expressive data-processing pipelines:

const laporanBulanan = katalog
  .filter((p) => p.stok > 0)               // Only available items
  .map((p) => ({                            // Transform into a report format
    nama: p.nama,
    pendapatan: p.harga * p.stok,
  }))
  .sort((a, b) => b.pendapatan - a.pendapatan) // Sort from highest
  .slice(0, 3);                             // Take the top 3

// TypeScript infers the final type: Array<{ nama: string; pendapatan: number }>

break and continue — Loop Flow Control #

break stops the loop entirely, continue skips the rest of the current iteration and moves on to the next one.

// break — exit the loop when a condition is met
const daftar = [3, 7, 2, 9, 1, 5, 8];
let posisiPertama = -1;

for (let i = 0; i < daftar.length; i++) {
  if (daftar[i] > 6) {
    posisiPertama = i;
    break; // Stop the search as soon as the first is found
  }
}
console.log(`Posisi pertama nilai > 6: ${posisiPertama}`); // 1

// continue — skip elements that don't qualify
const nilaiMahasiswa = [75, 45, 90, 30, 85, 60, 40];
const nilaiLulus: number[] = [];

for (const nilai of nilaiMahasiswa) {
  if (nilai < 60) continue; // Skip values below 60
  nilaiLulus.push(nilai);   // Only process values >= 60
}
console.log(nilaiLulus); // [75, 90, 85, 60]

// Labels for break/continue in nested loops
outer: for (let i = 0; i < 3; i++) {
  for (let j = 0; j < 3; j++) {
    if (i === 1 && j === 1) {
      break outer; // Exit BOTH loops at once
    }
    console.log(`i=${i}, j=${j}`);
  }
}
// i=0,j=0 | i=0,j=1 | i=0,j=2 | i=1,j=0 — then stops

Generators — Custom Iteration #

A generator is a function that can “pause” in the middle of execution using yield. It produces values one at a time and is very efficient for large datasets or unbounded value sequences:

// Generator function — marked with an asterisk (*)
function* urutanFibonacci(): Generator<number> {
  let [a, b] = [0, 1];
  while (true) {
    yield a;          // "Send" the value a, then pause
    [a, b] = [b, a + b];
  }
}

// Use the generator with for...of — TypeScript knows each value is a number
const fib = urutanFibonacci();
for (const angka of fib) {
  if (angka > 100) break; // Stop when passing 100
  process.stdout.write(angka + " ");
}
// 0 1 1 2 3 5 8 13 21 34 55 89

// A Python-style range generator
function* range(mulai: number, selesai: number, langkah = 1): Generator<number> {
  for (let i = mulai; i < selesai; i += langkah) {
    yield i;
  }
}

for (const i of range(0, 10, 2)) {
  process.stdout.write(i + " "); // 0 2 4 6 8
}

Choosing the Right Loop Construct #

flowchart TD
    A{"What do you want<br/>to iterate?"} --> B["Array or collection<br/>with values"]
    A --> C["Object with<br/>key-value pairs"]
    A --> D["Dynamic condition<br/>unknown iteration count"]
    A --> E["Large dataset or<br/>unbounded sequence"]

    B --> F{"Need transformation<br/>or filtering?"}
    F -- "Yes" --> G["map / filter / reduce<br/>or chaining"]
    F -- "No" --> H{"Need the index?"}

    H -- "Yes" --> I["for of with entries()"]
    H -- "No" --> J["for of"]

    C --> K["for in with<br/>hasOwnProperty<br/>or Object.entries()"]

    D --> L{"Must run<br/>at least once?"}
    L -- "Yes" --> M["do while"]
    L -- "No" --> N["while"]

    E --> O["Generator function<br/>with yield"]

    style G fill:#51cf66,color:#fff
    style I fill:#51cf66,color:#fff
    style J fill:#51cf66,color:#fff
    style K fill:#fcc419,color:#000
    style M fill:#339af0,color:#fff
    style N fill:#339af0,color:#fff
    style O fill:#cc5de8,color:#fff

Summary #

  • for...of is the primary choice for arrays — it’s type-safe, concise, and doesn’t have the scoping problems of for with var; use .entries() when you need the index too.
  • Avoid for...in for arrays — it returns keys as string, iterates prototype properties, and doesn’t guarantee order; use for...in only for plain objects with a hasOwnProperty check.
  • map, filter, reduce are more expressive than imperative loops for data transformation — the results are easier to chain, immutable, and type inference works automatically.
  • filter with a type guard ((v): v is T) produces an array with a more specific type, not just filtered elements — this matters when working with union types containing null/undefined.
  • The classic for is still relevant for backward iteration, non-standard steps, or when you need to modify an array in place during iteration.
  • while for dynamic conditions — conditions only known at runtime; always make sure something in the loop will turn the condition false, or add a maximum iteration limit.
  • do...while for at least one execution — suited to polling, interactive menus, or input validation that must be attempted at least once.
  • Generators for large datasets or unbounded sequences — they save memory by producing values one at a time (lazy evaluation), instead of storing the whole collection in memory at once.
  • break and continue control loop flow; use labels (outer:) in nested loops to exit more than one level at once.
  • Enable noUncheckedIndexedAccess in tsconfig.json to get warnings when array index access yields the type T | undefined — safer than the default T.

← Previous: Conditional Branching   Next: Functions →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact