List #
In TypeScript, list data structures are represented using arrays — the built-in data type with full support from the TypeScript type system. Unlike JavaScript where arrays can freely hold any value, TypeScript arrays have a clearly defined element type, so every operation — from push, map, filter, to sort — is type-validated by the compiler. Understanding TypeScript arrays deeply means understanding two philosophies that often collide: mutation (changing an existing array in place) vs immutability (producing a new array without changing the old one). This article covers both and when each is appropriate.
Defining Arrays #
TypeScript provides two syntaxes for declaring array types, both equivalent:
// Syntax 1: T[] — more concise, more commonly used
let angka: number[] = [1, 2, 3, 4, 5];
let nama: string[] = ["Budi", "Siti", "Ahmad"];
// Syntax 2: Array<T> — generic syntax, useful for complex types
let daftarPengguna: Array<{ id: string; nama: string }> = [];
let matriks: Array<Array<number>> = [[1, 2], [3, 4], [5, 6]];
// Union type array — elements can have more than one type
let campuran: (number | string)[] = [1, "dua", 3, "empat"];
// Array of objects with an interface
interface Produk {
id: string;
nama: string;
harga: number;
stok: number;
}
const katalog: Produk[] = [
{ id: "PRD-001", nama: "Kurma Ajwa", harga: 85_000, stok: 50 },
{ id: "PRD-002", nama: "Madu Sidr", harga: 250_000, stok: 12 },
{ id: "PRD-003", nama: "Minyak Zaitun", harga: 75_000, stok: 30 },
];
Type Inference on Arrays #
TypeScript infers an array’s type automatically from its initial value. Use explicit annotations only when the array starts empty or when inference produces a type that’s too wide:
// Type inference — TypeScript infers number[]
const nilaiOtomatis = [10, 20, 30]; // Type: number[]
// Needs an explicit annotation — the array starts empty
const daftarKosong: string[] = []; // Without an annotation → type: never[]
// ANTI-PATTERN: An unannotated array that starts empty
const daftarSalah = []; // Type: never[] — can't hold anything!
daftarSalah.push("halo"); // ✗ Error: Argument of type 'string' is not assignable to parameter of type 'never'
ReadonlyArray — Arrays That Can’t Be Modified
#
For arrays that shouldn’t change after creation, use ReadonlyArray<T> or the readonly T[] syntax:
// Two equivalent syntaxes
const daftarTetap: ReadonlyArray<string> = ["Senin", "Selasa", "Rabu"];
const hariKerja: readonly string[] = ["Senin", "Selasa", "Rabu", "Kamis", "Jumat"];
// All mutation methods are blocked by the compiler
// daftarTetap.push("Kamis"); // ✗ Error: Property 'push' does not exist on type 'readonly string[]'
// daftarTetap[0] = "Minggu"; // ✗ Error: Index signature in type 'readonly string[]' only permits reading
// daftarTetap.sort(); // ✗ Error: Property 'sort' does not exist on type 'readonly string[]'
// Methods that produce a NEW array are still available
const sorted = [...daftarTetap].sort(); // ✓ Make a copy first, then sort
Accessing Array Elements #
Array elements are accessed through zero-based indices. TypeScript with default configuration assumes index access always yields the element type (not undefined), which can cause subtle bugs:
const buah: string[] = ["mangga", "pisang", "jeruk"];
// Normal access
console.log(buah[0]); // "mangga"
console.log(buah[2]); // "jeruk"
// Trap: out-of-bounds index access — no compile error!
console.log(buah[10]); // undefined at runtime, but TypeScript thinks this is a string
// Solution: enable noUncheckedIndexedAccess in tsconfig.json
// With this option, buah[i] is typed string | undefined — more accurate
Safe Access with Destructuring #
To safely take the first element and the rest:
const [pertama, kedua, ...selebihnya] = buah;
// pertama: string, kedua: string, selebihnya: string[]
console.log(pertama); // "mangga"
console.log(kedua); // "pisang"
console.log(selebihnya); // ["jeruk"]
// For arrays that might be empty, use optional or defaults
const [kepala = "tidak ada"] = daftarKosong; // "tidak ada" if empty
Mutation vs Immutability #
This is the most important architectural decision when working with arrays. Mutation methods change the original array in place; immutable methods produce a new array without changing the old one.
Mutation Methods — Changing the Array In Place #
let angka: number[] = [3, 1, 4, 1, 5, 9, 2, 6];
// push / unshift — add elements
angka.push(5, 3, 5); // Add at the end — [3,1,4,1,5,9,2,6,5,3,5]
angka.unshift(0); // Add at the start — [0,3,1,4,1,5,9,2,6,5,3,5]
// pop / shift — remove elements
const akhir = angka.pop(); // Remove from the end, return the value → 5
const awal = angka.shift(); // Remove from the start, return the value → 0
// splice — remove, replace, or insert at a specific position
angka.splice(2, 2); // Remove 2 elements starting at index 2
angka.splice(1, 0, 99, 88); // Insert 99 and 88 at index 1
angka.splice(0, 1, 100); // Replace 1 element at index 0 with 100
// sort and reverse — change order in place
angka.sort((a, b) => a - b); // Sort ascending
angka.reverse(); // Reverse the order
Mutation methods likepush,pop,sort,reverse, andsplicechange the original array. This can cause hard-to-track bugs if the same array is referenced in many places. In functional programming and state management (React, Redux), always use immutable methods that produce a new array.
Immutable Methods — Producing a New Array #
const asli = [3, 1, 4, 1, 5, 9, 2, 6];
// Spread operator — the most idiomatic way to make a copy
const salinan = [...asli];
// concat — combine arrays
const digabung = asli.concat([5, 3, 5]); // New array
const digabung2 = [...asli, 5, 3, 5]; // Modern way with spread
// slice — take part of an array
const sebagian = asli.slice(2, 5); // [4, 1, 5] — indices 2 to 4
// filter — elements matching a condition
const genap = asli.filter((n) => n % 2 === 0); // [4, 2, 6]
// map — transform every element
const dikuadratkan = asli.map((n) => n ** 2); // [9, 1, 16, 1, 25, 81, 4, 36]
// Immutable sort — make a copy first, then sort
const terurut = [...asli].sort((a, b) => a - b); // The original doesn't change
console.log(asli); // [3, 1, 4, 1, 5, 9, 2, 6] — still the same
console.log(terurut); // [1, 1, 2, 3, 4, 5, 6, 9]
Functional Methods — The Core of Array Processing #
TypeScript provides full type inference on all functional array methods. This makes data-processing pipelines type-safe from end to end.
map — Element Transformation
#
const produk: Produk[] = katalog; // From the earlier example
// Transform into a display format
const tampilan = produk.map((p) => ({
label: p.nama,
harga: `Rp ${p.harga.toLocaleString("id-ID")}`,
tersedia: p.stok > 0,
}));
// Type: Array<{ label: string; harga: string; tersedia: boolean }>
// Map with the index
const bernomor = produk.map((p, i) => `${i + 1}. ${p.nama}`);
// ["1. Kurma Ajwa", "2. Madu Sidr", "3. Minyak Zaitun"]
filter — Filtering with Type Guards
#
// Regular filter — the result type matches the input type
const tersedia = produk.filter((p) => p.stok > 0);
// Type: Produk[]
// Filter with a type guard — narrows the result type
type NilaiMungkinNull = string | null | undefined;
const campuranData: NilaiMungkinNull[] = ["satu", null, "dua", undefined, "tiga"];
// Without a type guard — the result type is still (string | null | undefined)[]
const tanpaGuard = campuranData.filter((v) => v != null);
// With a type guard — the result type becomes string[]
const denganGuard = campuranData.filter((v): v is string => v != null);
console.log(denganGuard); // ["satu", "dua", "tiga"] — type: string[]
reduce — Accumulating Values
#
// Calculate the total shopping value
const totalStokNilai = produk.reduce((total, p) => {
return total + p.harga * p.stok;
}, 0);
// 85_000*50 + 250_000*12 + 75_000*30 = 4_250_000 + 3_000_000 + 2_250_000 = 9_500_000
// Reduce to an object — group by category
const perStok = produk.reduce(
(kelompok, p) => {
const kategori = p.stok > 20 ? "banyak" : p.stok > 5 ? "sedang" : "sedikit";
kelompok[kategori] = [...(kelompok[kategori] ?? []), p.nama];
return kelompok;
},
{} as Record<string, string[]>
);
// { banyak: ["Kurma Ajwa", "Minyak Zaitun"], sedang: ["Madu Sidr"] }
flatMap — Map Then Flatten
#
const kalimat = ["halo dunia", "typescript itu keren", "belajar setiap hari"];
// map produces string[][] — an array of arrays
const kata2D = kalimat.map((k) => k.split(" "));
// [["halo", "dunia"], ["typescript", "itu", "keren"], ["belajar", "setiap", "hari"]]
// flatMap produces string[] — flat directly
const kataFlat = kalimat.flatMap((k) => k.split(" "));
// ["halo", "dunia", "typescript", "itu", "keren", "belajar", "setiap", "hari"]
// flatMap is also useful for transformations yielding zero or more elements
const hargaValid = produk.flatMap((p) =>
p.harga > 0 && p.stok > 0 ? [p.harga] : [] // Only prices of valid, available products
);
Chaining Methods — Data Pipelines #
const laporanTop3 = katalog
.filter((p) => p.stok > 0) // 1. Only available items
.map((p) => ({ // 2. Calculate stock value
nama: p.nama,
nilaiStok: p.harga * p.stok,
hargaFormatted: `Rp ${p.harga.toLocaleString("id-ID")}`,
}))
.sort((a, b) => b.nilaiStok - a.nilaiStok) // 3. Sort from largest
.slice(0, 3); // 4. Take the top 3
// TypeScript infers the final type:
// Array<{ nama: string; nilaiStok: number; hargaFormatted: string }>
Searching for Elements #
TypeScript has several search methods with different return types:
const angka = [10, 25, 3, 47, 8, 31, 15];
// indexOf — returns the first index or -1
const indeks = angka.indexOf(25); // 1
const tidakAda = angka.indexOf(99); // -1
// findIndex — search by condition, return the index
const idxBesar = angka.findIndex((n) => n > 30); // 3 (the value 47)
// find — search by condition, return the VALUE or undefined
const nilaiPertama = angka.find((n) => n > 20); // 25
// Type: number | undefined — TypeScript knows it might not be found
// findLast / findLastIndex (ES2023) — search from the back
const nilaiTerakhirBesar = angka.findLast((n) => n > 20); // 31
// includes — check value existence
const adaTiga = angka.includes(3); // true
const adaSepuluh = angka.includes(10); // true
// some / every — check conditions
const adaYangBesar = angka.some((n) => n > 40); // true (47 exists)
const semuaPositif = angka.every((n) => n > 0); // true
const semuaBesar = angka.every((n) => n > 20); // false
Sorting with Comparators #
The sort() method without arguments sorts by string representation — this is a classic trap for numeric arrays:
// ANTI-PATTERN: sort() without a comparator for numbers — classic bug
const angkaSalah = [10, 9, 2, 21, 3];
angkaSalah.sort();
console.log(angkaSalah); // [10, 2, 21, 3, 9] — WRONG! Sorted as strings
// CORRECT: Use a comparator for numbers
const angkaBenar = [10, 9, 2, 21, 3];
angkaBenar.sort((a, b) => a - b); // Ascending
console.log(angkaBenar); // [2, 3, 9, 10, 21]
angkaBenar.sort((a, b) => b - a); // Descending
console.log(angkaBenar); // [21, 10, 9, 3, 2]
// Sort an array of objects by a property
const terurut = [...katalog].sort((a, b) => a.harga - b.harga); // Ascending by price
const terurutNama = [...katalog].sort((a, b) =>
a.nama.localeCompare(b.nama, "id") // Sort strings with the Indonesian locale
);
Set — Collections of Unique Values
#
Set is a data structure that only stores unique values — no duplicates. TypeScript supports Set<T> with full type safety:
// Creating a Set
const angkaUnik = new Set<number>([1, 2, 3, 2, 1, 4]);
console.log(angkaUnik.size); // 4 — duplicates removed automatically
console.log([...angkaUnik]); // [1, 2, 3, 4]
// Operations on a Set
const kategori = new Set<string>();
kategori.add("makanan");
kategori.add("minuman");
kategori.add("makanan"); // Not added — already exists
console.log(kategori.has("makanan")); // true
console.log(kategori.size); // 2
kategori.delete("minuman");
console.log([...kategori]); // ["makanan"]
// Common pattern: remove duplicates from an array
const arrayDenganDuplikat = [1, 2, 3, 2, 1, 4, 3, 5];
const tanpaDuplikat = [...new Set(arrayDenganDuplikat)];
// [1, 2, 3, 4, 5]
// Set operations
const setA = new Set([1, 2, 3, 4, 5]);
const setB = new Set([3, 4, 5, 6, 7]);
// Union
const union = new Set([...setA, ...setB]);
// {1, 2, 3, 4, 5, 6, 7}
// Intersection
const intersection = new Set([...setA].filter((x) => setB.has(x)));
// {3, 4, 5}
// Difference
const difference = new Set([...setA].filter((x) => !setB.has(x)));
// {1, 2}
Multidimensional Arrays #
TypeScript supports multidimensional arrays with consistent types across all dimensions:
// 2D matrix — an array of arrays
const matriks: number[][] = [
[1, 2, 3],
[4, 5, 6],
[7, 8, 9],
];
// Element access
console.log(matriks[1][2]); // 6 — row 1, column 2
// Matrix iteration
for (const baris of matriks) {
for (const nilai of baris) {
process.stdout.write(`${nilai} `);
}
console.log();
}
// Matrix transpose — an example of 2D transformation
function transpose(mat: number[][]): number[][] {
return mat[0].map((_, kolomIdx) => mat.map((baris) => baris[kolomIdx]));
}
console.log(transpose(matriks));
// [[1, 4, 7], [2, 5, 8], [3, 6, 9]]
// Array of tuples — for lightweight structured data
const koordinat: [number, number][] = [
[-6.2088, 106.8456], // Jakarta
[-7.2575, 112.7521], // Surabaya
[-6.9147, 107.6098], // Bandung
];
for (const [lat, lon] of koordinat) {
console.log(`Lat: ${lat}, Lon: ${lon}`);
}
Array Method Map #
flowchart TD
A[TypeScript Array Methods] --> B[Mutation - Change In-Place]
A --> C[Functional - Produce New Arrays]
A --> D[Searching]
A --> E[Aggregation]
B --> B1[push - add to end]
B --> B2[pop - remove from end]
B --> B3[unshift - add to start]
B --> B4[shift - remove from start]
B --> B5[splice - insert-remove]
B --> B6[sort - sort]
B --> B7[reverse - reverse]
C --> C1[map - transform]
C --> C2[filter - filter]
C --> C3[flatMap - map + flatten]
C --> C4[slice - take a part]
C --> C5[concat - combine]
C --> C6[spread ... - copy]
D --> D1[find - first value]
D --> D2[findIndex - first index]
D --> D3[indexOf - exact index]
D --> D4[includes - check existence]
D --> D5[some - any matching?]
D --> D6[every - all matching?]
E --> E1[reduce - accumulate]
E --> E2[join - join into a string]
E --> E3[length - element count]
style B fill:#ff6b6b,color:#fff
style C fill:#51cf66,color:#fff
style D fill:#339af0,color:#fff
style E fill:#fcc419,color:#000Summary #
T[]vsArray<T>— both are equivalent; useT[]for readability in most cases,Array<T>when you need more complex generics.- Declare types for empty arrays —
const daftar: string[] = []; without an annotation, TypeScript infersnever[]which can’t hold anything.ReadonlyArray<T>orreadonly T[]— for arrays that must not be modified after creation; all mutation methods are blocked by the compiler.- Mutation methods (
push,pop,sort,splice) change the original array — be careful if the array is referenced in many places; use[...arr].sort()for sorting operations that don’t modify the original.filterwith a type guard(v): v is Tproduces an array with a more specific type — important when filteringnullorundefinedout of a union type.- Don’t use
sort()without a comparator for numbers —[10, 9, 2].sort()produces[10, 2, 9]because it sorts as strings; always usesort((a, b) => a - b).flatMapis more efficient thanmapfollowed byflat— use it when a transformation produces arrays that need to be flattened one level.Setfor unique values —[...new Set(arr)]is the most concise way to remove duplicates from an array;Setalso supports set operations (union, intersection, difference).- Chaining array methods (
filter → map → sort → slice) forms an expressive, type-safe pipeline — TypeScript infers types at every stage automatically.- Enable
noUncheckedIndexedAccessintsconfig.jsonsoarr[i]access yieldsT | undefinedinstead of justT— safer for preventing out-of-bounds access bugs.