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 usefor...ofinstead offor...in.for...inreturns keys asstring(notnumber), iterates properties inherited from the prototype, and its order isn’t guaranteed consistent across JavaScript engines. Usefor...inonly for plain objects, and always include ahasOwnPropertycheck.
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:#fffSummary #
for...ofis the primary choice for arrays — it’s type-safe, concise, and doesn’t have the scoping problems offorwithvar; use.entries()when you need the index too.- Avoid
for...infor arrays — it returns keys asstring, iterates prototype properties, and doesn’t guarantee order; usefor...inonly for plain objects with ahasOwnPropertycheck.map,filter,reduceare more expressive than imperative loops for data transformation — the results are easier to chain, immutable, and type inference works automatically.filterwith 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
foris still relevant for backward iteration, non-standard steps, or when you need to modify an array in place during iteration.whilefor dynamic conditions — conditions only known at runtime; always make sure something in the loop will turn the conditionfalse, or add a maximum iteration limit.do...whilefor 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.
breakandcontinuecontrol loop flow; use labels (outer:) in nested loops to exit more than one level at once.- Enable
noUncheckedIndexedAccessintsconfig.jsonto get warnings when array index access yields the typeT | undefined— safer than the defaultT.