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 like push, pop, sort, reverse, and splice change 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:#000

Summary #

  • T[] vs Array<T> — both are equivalent; use T[] for readability in most cases, Array<T> when you need more complex generics.
  • Declare types for empty arraysconst daftar: string[] = []; without an annotation, TypeScript infers never[] which can’t hold anything.
  • ReadonlyArray<T> or readonly 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.
  • filter with a type guard (v): v is T produces an array with a more specific type — important when filtering null or undefined out 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 use sort((a, b) => a - b).
  • flatMap is more efficient than map followed by flat — use it when a transformation produces arrays that need to be flattened one level.
  • Set for unique values[...new Set(arr)] is the most concise way to remove duplicates from an array; Set also 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 noUncheckedIndexedAccess in tsconfig.json so arr[i] access yields T | undefined instead of just T — safer for preventing out-of-bounds access bugs.

← Previous: Exceptions   Next: Map →

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