Map #

Map adalah struktur data kunci-nilai yang diperkenalkan di ES2015 dan mendapat dukungan penuh dari sistem tipe TypeScript melalui generic Map<K, V>. Berbeda dari plain object JavaScript yang hanya bisa menggunakan string atau Symbol sebagai kunci, Map bisa menggunakan nilai apapun sebagai kunci — termasuk object, fungsi, atau bahkan Map lain. TypeScript memastikan bahwa kunci dan nilai selalu bertipe yang sesuai dengan deklarasi, sehingga akses dan manipulasi Map menjadi sepenuhnya type-safe. Memahami kapan menggunakan Map versus plain object — dan kapan menggunakan WeakMap — adalah keterampilan penting untuk menulis kode TypeScript yang efisien dan idiomatik.

Membuat Map dengan Type Parameter #

Deklarasikan Map dengan type parameter <K, V> untuk menentukan tipe kunci dan nilai secara eksplisit:

// Map<kunci, nilai> — deklarasi eksplisit
const kamus = new Map<string, string>();
const skorPemain = new Map<string, number>();
const konfigurasi = new Map<string, boolean | string | number>();

// Inisialisasi dengan data awal — array of tuples [kunci, nilai]
const hariKerja = new Map<number, string>([
  [1, "Senin"],
  [2, "Selasa"],
  [3, "Rabu"],
  [4, "Kamis"],
  [5, "Jumat"],
]);

// Map dengan object sebagai nilai
interface DataPengguna {
  nama: string;
  email: string;
  aktif: boolean;
}

const registriPengguna = new Map<string, DataPengguna>([
  ["usr-001", { nama: "Budi Santoso", email: "[email protected]", aktif: true }],
  ["usr-002", { nama: "Siti Rahma", email: "[email protected]", aktif: false }],
]);

// Type inference — TypeScript menyimpulkan tipe dari nilai awal
const inferensiOtomatis = new Map([
  ["nama", "Budi"],
  ["kota", "Jakarta"],
]);
// Tipe: Map<string, string> — disimpulkan otomatis

Operasi CRUD — Tambah, Baca, Perbarui, Hapus #

set — Menambah atau Memperbarui #

const inventori = new Map<string, { stok: number; harga: number }>();

// Menambah entri baru
inventori.set("PRD-001", { stok: 50, harga: 85_000 });
inventori.set("PRD-002", { stok: 12, harga: 250_000 });
inventori.set("PRD-003", { stok: 30, harga: 75_000 });

// Memperbarui entri yang sudah ada — set() menggantikan nilai lama
inventori.set("PRD-001", { stok: 45, harga: 85_000 }); // Stok berkurang 5

// Chaining set() — Map.set() mengembalikan Map itu sendiri
const konfigurasi = new Map<string, string>()
  .set("host", "localhost")
  .set("port", "5432")
  .set("nama_db", "muslimapps");

get — Mengambil Nilai #

const hargaProduk = new Map<string, number>([
  ["kurma", 85_000],
  ["madu", 250_000],
  ["minyak", 75_000],
]);

// get() mengembalikan nilai | undefined — selalu cek sebelum digunakan
const hargaKurma = hargaProduk.get("kurma");
// Tipe: number | undefined

// ANTI-PATTERN: Langsung gunakan nilai tanpa cek
// const diskon = hargaProduk.get("item-baru") * 0.1; // ✗ Error: mungkin undefined

// BENAR: Cek keberadaan sebelum menggunakan
const hargaTarget = hargaProduk.get("madu");
if (hargaTarget !== undefined) {
  console.log(`Harga madu: Rp ${hargaTarget.toLocaleString("id-ID")}`);
}

// Atau gunakan nullish coalescing untuk nilai default
const hargaDenganDefault = hargaProduk.get("produk-baru") ?? 0;

has — Memeriksa Keberadaan #

const sesiAktif = new Map<string, { userId: string; loginPada: Date }>();
sesiAktif.set("token-abc123", { userId: "usr-001", loginPada: new Date() });

// Pola umum: cek dulu sebelum get() untuk menghindari nilai undefined
function ambilSesi(token: string) {
  if (!sesiAktif.has(token)) {
    console.log("Token tidak valid atau sudah kedaluwarsa");
    return null;
  }

  // Setelah has() mengonfirmasi keberadaan, get() aman digunakan
  // Tapi TypeScript masih mengembalikan T | undefined — perlu non-null assertion
  return sesiAktif.get(token)!;
}

delete dan clear — Menghapus #

const cache = new Map<string, { data: unknown; kedaluwarsa: number }>();

cache.set("kunci-1", { data: { nama: "Budi" }, kedaluwarsa: Date.now() + 60_000 });
cache.set("kunci-2", { data: [1, 2, 3], kedaluwarsa: Date.now() + 30_000 });

// Hapus satu entri berdasarkan kunci
const berhasilDihapus = cache.delete("kunci-1"); // true jika ada, false jika tidak
console.log(`Dihapus: ${berhasilDihapus}`); // true

// Hapus entri yang sudah kedaluwarsa
function bersihkanCache(): void {
  const sekarang = Date.now();
  for (const [kunci, nilai] of cache) {
    if (nilai.kedaluwarsa < sekarang) {
      cache.delete(kunci);
    }
  }
}

// Hapus semua entri sekaligus
cache.clear();
console.log(cache.size); // 0

Iterasi pada Map #

Map menjaga urutan elemen sesuai urutan penambahan — ini berbeda dari plain object yang urutannya tidak sepenuhnya dijamin. Tersedia beberapa cara iterasi:

const populasiKota = new Map<string, number>([
  ["Jakarta", 10_560_000],
  ["Surabaya", 2_890_000],
  ["Bandung", 2_490_000],
  ["Medan", 2_450_000],
  ["Bekasi", 2_340_000],
]);

// 1. for...of pada Map — mengiterasi [kunci, nilai] setiap iterasi
for (const [kota, populasi] of populasiKota) {
  // kota: string, populasi: number — TypeScript tahu tipenya
  console.log(`${kota}: ${populasi.toLocaleString("id-ID")} jiwa`);
}

// 2. forEach — callback dengan parameter (nilai, kunci, map)
populasiKota.forEach((populasi, kota) => {
  // Perhatikan: urutan parameter forEach adalah (nilai, kunci) — terbalik dari for...of!
  console.log(`${kota}: ${populasi.toLocaleString("id-ID")}`);
});

// 3. Iterasi kunci saja
for (const kota of populasiKota.keys()) {
  console.log(kota); // "Jakarta", "Surabaya", ...
}

// 4. Iterasi nilai saja
for (const populasi of populasiKota.values()) {
  console.log(populasi); // 10560000, 2890000, ...
}

// 5. Iterasi sebagai entries() — ekuivalen dengan for...of langsung
for (const [kota, populasi] of populasiKota.entries()) {
  console.log(`${kota}: ${populasi}`);
}
Perhatikan perbedaan urutan parameter antara for...of dan forEach: pada for...of, destructuring menghasilkan [kunci, nilai]; tapi pada forEach, callback menerima (nilai, kunci, map) — kunci dan nilai terbalik. Ini adalah sumber bug yang halus dan sering diabaikan.

Kunci Non-String — Keunggulan Utama Map #

Kemampuan menggunakan tipe apapun sebagai kunci adalah keunggulan Map yang tidak dimiliki plain object:

// Kunci berupa object — sangat berguna untuk WeakRef dan caching
const cache = new Map<object, string>();

const objA = { id: 1 };
const objB = { id: 2 };

cache.set(objA, "data untuk objek A");
cache.set(objB, "data untuk objek B");

console.log(cache.get(objA)); // "data untuk objek A"
console.log(cache.get({ id: 1 })); // undefined — bukan objek yang sama!
// Map menggunakan referensi untuk membandingkan kunci object, bukan nilai

// Kunci berupa fungsi
type FungsiHandler = () => void;
const deskripsiHandler = new Map<FungsiHandler, string>();

const handlerKlik = () => console.log("diklik");
const handlerHover = () => console.log("dihover");

deskripsiHandler.set(handlerKlik, "Handler untuk event klik tombol");
deskripsiHandler.set(handlerHover, "Handler untuk event hover elemen");

// Kunci berupa number — langsung, tanpa konversi ke string seperti object biasa
const bulanIndonesia = new Map<number, string>([
  [1, "Januari"], [2, "Februari"], [3, "Maret"],
  [4, "April"],   [5, "Mei"],      [6, "Juni"],
  [7, "Juli"],    [8, "Agustus"],  [9, "September"],
  [10, "Oktober"],[11, "November"],[12, "Desember"],
]);

const bulanSekarang = bulanIndonesia.get(new Date().getMonth() + 1);
console.log(`Bulan sekarang: ${bulanSekarang}`);

Map vs Plain Object — Kapan Memilih Masing-Masing #

Ini adalah pertanyaan yang sering muncul. Keduanya menyimpan pasangan kunci-nilai, tapi punya karakteristik yang berbeda:

AspekMap<K, V>Plain Object {}
Tipe kunciSemua tipestring, number, Symbol
Urutan kunciDijamin — urutan insertTidak dijamin sepenuhnya
Properti sizemap.size✗ Perlu Object.keys(obj).length
IterasiLangsung dengan for...ofPerlu Object.entries()
Performa insert/deleteLebih baik untuk data dinamisLebih baik untuk struktur tetap
Serialisasi JSONJSON.stringify(map){}✓ Langsung JSON.stringify(obj)
Prototype pollution✓ Tidak ada risiko✗ Ada risiko (nama kunci bisa bentrok)
Kasus penggunaanCache, registry, data dinamisKonfigurasi, DTO, object tetap
// Gunakan Map untuk: data yang sering berubah, kunci dinamis, perlu size
const cacheHasil = new Map<string, number>(); // ✓
cacheHasil.set(hitungKompleks(), 42);         // Kunci dinamis

// Gunakan plain object untuk: konfigurasi tetap, serialisasi JSON, DTO
const konfig = {
  host: "localhost",
  port: 5432,
};
JSON.stringify(konfig); // ✓ Bekerja langsung

// ANTI-PATTERN: Gunakan Map untuk konfigurasi statis yang diketahui
// const konfigSalah = new Map([["host", "localhost"], ["port", 5432]]);
// — Verbos dan tidak bisa langsung di-JSON.stringify

WeakMap — Map yang Bersahabat dengan Garbage Collector #

WeakMap mirip dengan Map tapi dengan perbedaan krusial: kuncinya harus berupa object, dan referensi ke kunci bersifat weak — jika tidak ada referensi lain ke object kunci, garbage collector bisa menghapusnya bersama dengan entri WeakMap terkait. Ini mencegah memory leak untuk data yang terkait dengan lifecycle object tertentu:

// WeakMap — kunci harus object, nilainya bisa tipe apapun
const metadataDOM = new WeakMap<Element, { diklik: number; dilihat: number }>();

// Saat element dihapus dari DOM, entri WeakMap otomatis dibersihkan
// Tidak ada memory leak!
function lacakInteraksi(el: Element): void {
  const meta = metadataDOM.get(el) ?? { diklik: 0, dilihat: 0 };
  meta.dilihat++;
  metadataDOM.set(el, meta);
}

// WeakMap tidak bisa diiterasi — tidak ada .forEach(), .keys(), .values()
// Ini adalah trade-off untuk mendapatkan garbage collection otomatis

// Pola umum: menyimpan data privat untuk instance kelas
const _dataPrivat = new WeakMap<Kelas, { nilaiRahasia: string }>();

class Kelas {
  constructor(rahasia: string) {
    _dataPrivat.set(this, { nilaiRahasia: rahasia });
  }

  ambilRahasia(): string {
    return _dataPrivat.get(this)!.nilaiRahasia;
  }
}

const instance = new Kelas("data-sensitif");
console.log(instance.ambilRahasia()); // "data-sensitif"
// _dataPrivat.get(instance) hanya bisa diakses dari dalam modul yang mendefinisikan WeakMap

Pola Umum Penggunaan Map #

Pola 1: Cache dengan Batas Waktu #

interface EntriCache<T> {
  nilai: T;
  kedaluwarsa: number; // timestamp Unix
}

class Cache<K, V> {
  private store = new Map<K, EntriCache<V>>();

  set(kunci: K, nilai: V, ttlDetik: number): void {
    this.store.set(kunci, {
      nilai,
      kedaluwarsa: Date.now() + ttlDetik * 1000,
    });
  }

  get(kunci: K): V | null {
    const entri = this.store.get(kunci);
    if (!entri) return null;

    if (Date.now() > entri.kedaluwarsa) {
      this.store.delete(kunci); // Hapus entri yang kedaluwarsa
      return null;
    }

    return entri.nilai;
  }

  get ukuran(): number {
    return this.store.size;
  }
}

const cacheHarga = new Cache<string, number>();
cacheHarga.set("harga-kurma", 85_000, 300); // TTL 5 menit

const harga = cacheHarga.get("harga-kurma");
if (harga !== null) {
  console.log(`Harga dari cache: Rp ${harga.toLocaleString("id-ID")}`);
}

Pola 2: Frequency Counter #

function hitungFrekuensi<T>(arr: T[]): Map<T, number> {
  const frekuensi = new Map<T, number>();

  for (const item of arr) {
    frekuensi.set(item, (frekuensi.get(item) ?? 0) + 1);
  }

  return frekuensi;
}

const kata = ["satu", "dua", "satu", "tiga", "dua", "satu"];
const frekuensiKata = hitungFrekuensi(kata);

// Urutkan berdasarkan frekuensi terbanyak
const terurut = [...frekuensiKata.entries()]
  .sort(([, a], [, b]) => b - a);

for (const [kata, jumlah] of terurut) {
  console.log(`"${kata}": ${jumlah}x`);
}
// "satu": 3x
// "dua": 2x
// "tiga": 1x

Pola 3: Group By #

interface Transaksi {
  id: string;
  kategori: string;
  jumlah: number;
  tanggal: string;
}

function groupBy<T, K>(
  arr: T[],
  kunciDari: (item: T) => K
): Map<K, T[]> {
  const hasil = new Map<K, T[]>();

  for (const item of arr) {
    const kunci = kunciDari(item);
    const kelompok = hasil.get(kunci) ?? [];
    kelompok.push(item);
    hasil.set(kunci, kelompok);
  }

  return hasil;
}

const transaksi: Transaksi[] = [
  { id: "T1", kategori: "makanan", jumlah: 85_000, tanggal: "2025-05-01" },
  { id: "T2", kategori: "transport", jumlah: 25_000, tanggal: "2025-05-01" },
  { id: "T3", kategori: "makanan", jumlah: 120_000, tanggal: "2025-05-02" },
  { id: "T4", kategori: "hiburan", jumlah: 50_000, tanggal: "2025-05-02" },
];

const perKategori = groupBy(transaksi, (t) => t.kategori);

for (const [kategori, daftar] of perKategori) {
  const total = daftar.reduce((sum, t) => sum + t.jumlah, 0);
  console.log(`${kategori}: Rp ${total.toLocaleString("id-ID")} (${daftar.length} transaksi)`);
}
// makanan: Rp 205.000 (2 transaksi)
// transport: Rp 25.000 (1 transaksi)
// hiburan: Rp 50.000 (1 transaksi)

Konversi antara Map dan Array/Object #

const populasi = new Map<string, number>([
  ["Jakarta", 10_560_000],
  ["Surabaya", 2_890_000],
]);

// Map → Array of tuples
const sebagaiTuples = [...populasi];
// [["Jakarta", 10560000], ["Surabaya", 2890000]]

// Map → Array of keys
const kunci = [...populasi.keys()]; // ["Jakarta", "Surabaya"]

// Map → Array of values
const nilai = [...populasi.values()]; // [10560000, 2890000]

// Map → Plain object (hanya untuk kunci string)
const sebagaiObject = Object.fromEntries(populasi);
// { Jakarta: 10560000, Surabaya: 2890000 }

// Plain object → Map
const objKonfig = { host: "localhost", port: "5432" };
const mapKonfig = new Map(Object.entries(objKonfig));
// Map { "host" => "localhost", "port" => "5432" }

// CATATAN: Map tidak bisa langsung di-JSON.stringify
// JSON.stringify(populasi); // "{}" — hasilnya object kosong!

// Solusi: konversi ke object/array dulu
JSON.stringify(Object.fromEntries(populasi));
// '{"Jakarta":10560000,"Surabaya":2890000}'

Perbandingan Map, WeakMap, dan Plain Object #

flowchart TD
    A{"Struktur data<br/>kunci-nilai"} --> B{"Tipe kunci?"}

    B -- "Hanya string atau symbol" --> C{"Konfigurasi tetap<br/>atau serialisasi JSON?"}
    B -- "Tipe apa pun termasuk object" --> D{"Perlu garbage<br/>collection otomatis?"}

    C -- "Ya" --> E["Plain Object<br/>key dan value"]
    C -- "Tidak, data dinamis" --> F["Map dengan string key"]

    D -- "Ya, data terkait lifecycle object" --> G["WeakMap"]
    D -- "Tidak, perlu iterasi" --> H["Map dengan object key"]

    style E fill:#51cf66,color:#fff
    style F fill:#339af0,color:#fff
    style G fill:#cc5de8,color:#fff
    style H fill:#339af0,color:#fff

Ringkasan #

  • Selalu deklarasikan type parameter Map<K, V> secara eksplisit — new Map<string, number>() jauh lebih type-safe daripada new Map() yang menghasilkan Map<unknown, unknown>.
  • get() mengembalikan T | undefined — selalu cek dengan has() terlebih dahulu atau gunakan ?? nilaiDefault sebelum menggunakan nilai yang dikembalikan.
  • Urutan parameter forEach terbalik dari for...offorEach((nilai, kunci) => ...) berbeda dari for (const [kunci, nilai] of map) — jangan tertukar.
  • Gunakan Map untuk data dinamis (registry, cache, counter); gunakan plain object untuk konfigurasi statis yang diketahui di compile time dan perlu di-serialize ke JSON.
  • Map tidak bisa langsung di-JSON.stringify — konversi ke plain object dengan Object.fromEntries(map) dulu sebelum serialisasi.
  • Kunci object di Map dibandingkan berdasarkan referensi, bukan nilai — map.get({ id: 1 }) tidak akan menemukan entri yang diset dengan map.set({ id: 1 }, ...) karena keduanya adalah object yang berbeda.
  • WeakMap untuk data terkait lifecycle object — jika kamu menyimpan metadata untuk DOM element atau instance kelas dan tidak ingin memory leak, WeakMap adalah pilihan yang tepat karena entri dibersihkan otomatis saat kunci tidak lagi direferensikan.
  • Pola frequency counter map.set(k, (map.get(k) ?? 0) + 1) adalah idiom yang sangat umum — gunakan nullish coalescing untuk nilai default yang aman.
  • groupBy dengan Map menghasilkan struktur yang lebih efisien dari nested object karena iterasi lebih cepat dan tipe lebih ekspresif.

← Sebelumnya: List   Berikutnya: Date & Time →

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