Map #

Map is the key-value data structure introduced in ES2015, with full support from the TypeScript type system through the generic Map<K, V>. Unlike plain JavaScript objects that can only use strings or Symbols as keys, Map can use any value as a key — including objects, functions, or even other Maps. TypeScript ensures that keys and values always match their declared types, making Map access and manipulation fully type-safe. Understanding when to use Map versus a plain object — and when to use WeakMap — is an important skill for writing efficient, idiomatic TypeScript code.

Creating a Map with Type Parameters #

Declare a Map with type parameters <K, V> to explicitly define the key and value types:

// Map<key, value> — explicit declaration
const kamus = new Map<string, string>();
const skorPemain = new Map<string, number>();
const konfigurasi = new Map<string, boolean | string | number>();

// Initialize with initial data — an array of [key, value] tuples
const hariKerja = new Map<number, string>([
  [1, "Senin"],
  [2, "Selasa"],
  [3, "Rabu"],
  [4, "Kamis"],
  [5, "Jumat"],
]);

// A Map with objects as values
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 infers the type from the initial values
const inferensiOtomatis = new Map([
  ["nama", "Budi"],
  ["kota", "Jakarta"],
]);
// Type: Map<string, string> — inferred automatically

CRUD Operations — Add, Read, Update, Delete #

set — Adding or Updating #

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

// Adding a new entry
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 });

// Updating an existing entry — set() replaces the old value
inventori.set("PRD-001", { stok: 45, harga: 85_000 }); // Stock decreased by 5

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

get — Retrieving Values #

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

// get() returns value | undefined — always check before use
const hargaKurma = hargaProduk.get("kurma");
// Type: number | undefined

// ANTI-PATTERN: Using the value directly without a check
// const diskon = hargaProduk.get("item-baru") * 0.1; // ✗ Error: possibly undefined

// CORRECT: Check existence before using
const hargaTarget = hargaProduk.get("madu");
if (hargaTarget !== undefined) {
  console.log(`Harga madu: Rp ${hargaTarget.toLocaleString("id-ID")}`);
}

// Or use nullish coalescing for a default value
const hargaDenganDefault = hargaProduk.get("produk-baru") ?? 0;

has — Checking Existence #

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

// Common pattern: check with has() before get() to avoid undefined
function ambilSesi(token: string) {
  if (!sesiAktif.has(token)) {
    console.log("Token tidak valid atau sudah kedaluwarsa");
    return null;
  }

  // After has() confirms existence, get() is safe to use
  // But TypeScript still returns T | undefined — a non-null assertion is needed
  return sesiAktif.get(token)!;
}

delete and clear — Removing #

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 });

// Remove one entry by key
const berhasilDihapus = cache.delete("kunci-1"); // true if present, false if not
console.log(`Dihapus: ${berhasilDihapus}`); // true

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

// Remove all entries at once
cache.clear();
console.log(cache.size); // 0

Iterating over a Map #

Map maintains element order according to insertion order — unlike plain objects whose order isn’t fully guaranteed. Several iteration methods are available:

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 on a Map — iterates [key, value] each iteration
for (const [kota, populasi] of populasiKota) {
  // kota: string, populasi: number — TypeScript knows the types
  console.log(`${kota}: ${populasi.toLocaleString("id-ID")} jiwa`);
}

// 2. forEach — a callback with (value, key, map) parameters
populasiKota.forEach((populasi, kota) => {
  // Note: the forEach parameter order is (value, key) — reversed from for...of!
  console.log(`${kota}: ${populasi.toLocaleString("id-ID")}`);
});

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

// 4. Iterate values only
for (const populasi of populasiKota.values()) {
  console.log(populasi); // 10560000, 2890000, ...
}

// 5. Iterate as entries() — equivalent to direct for...of
for (const [kota, populasi] of populasiKota.entries()) {
  console.log(`${kota}: ${populasi}`);
}
Note the parameter order difference between for...of and forEach: with for...of, destructuring yields [key, value]; but with forEach, the callback receives (value, key, map) — key and value are reversed. This is a subtle, often-overlooked source of bugs.

Non-String Keys — A Major Advantage of Map #

The ability to use any type as a key is an advantage Map has over plain objects:

// Object keys — very useful for WeakRef and 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 — not the same object!
// Map compares object keys by reference, not by value

// Function keys
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");

// Number keys — direct, without converting to strings like plain objects
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 — When to Choose Which #

This is a question that comes up often. Both store key-value pairs, but they have different characteristics:

AspectMap<K, V>Plain Object {}
Key typesAll typesstring, number, Symbol
Key orderGuaranteed — insertion orderNot fully guaranteed
size propertymap.size✗ Needs Object.keys(obj).length
IterationDirect with for...ofNeeds Object.entries()
Insert/delete performanceBetter for dynamic dataBetter for fixed structures
JSON serializationJSON.stringify(map){}✓ Direct JSON.stringify(obj)
Prototype pollution✓ No risk✗ Risk (key names can collide)
Use casesCache, registry, dynamic dataConfig, DTOs, fixed objects
// Use Map for: frequently changing data, dynamic keys, need for size
const cacheHasil = new Map<string, number>(); // ✓
cacheHasil.set(hitungKompleks(), 42);         // Dynamic keys

// Use a plain object for: fixed config, JSON serialization, DTOs
const konfig = {
  host: "localhost",
  port: 5432,
};
JSON.stringify(konfig); // ✓ Works directly

// ANTI-PATTERN: Using a Map for static, known configuration
// const konfigSalah = new Map([["host", "localhost"], ["port", 5432]]);
// — Verbose and can't be directly JSON.stringify'd

WeakMap — A Garbage-Collector-Friendly Map #

WeakMap is similar to Map but with a crucial difference: its keys must be objects, and references to the keys are weak — if there are no other references to a key object, the garbage collector can remove it along with the associated WeakMap entry. This prevents memory leaks for data tied to the lifecycle of specific objects:

// WeakMap — keys must be objects, values can be any type
const metadataDOM = new WeakMap<Element, { diklik: number; dilihat: number }>();

// When an element is removed from the DOM, its WeakMap entry is cleaned up automatically
// No memory leak!
function lacakInteraksi(el: Element): void {
  const meta = metadataDOM.get(el) ?? { diklik: 0, dilihat: 0 };
  meta.dilihat++;
  metadataDOM.set(el, meta);
}

// WeakMap can't be iterated — no .forEach(), .keys(), .values()
// This is the trade-off for automatic garbage collection

// Common pattern: storing private data for class instances
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) can only be accessed inside the module defining the WeakMap

Common Map Usage Patterns #

Pattern 1: Cache with an Expiration Time #

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

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); // Remove the expired entry
      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); // 5-minute TTL

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

Pattern 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);

// Sort by highest frequency
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

Pattern 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)

Converting Between Map and Arrays/Objects #

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 (only for string keys)
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" }

// NOTE: A Map can't be directly JSON.stringify'd
// JSON.stringify(populasi); // "{}" — the result is an empty object!

// Solution: convert to an object/array first
JSON.stringify(Object.fromEntries(populasi));
// '{"Jakarta":10560000,"Surabaya":2890000}'

Comparing Map, WeakMap, and Plain Objects #

flowchart TD
    A{"Key-value<br/>data structure"} --> B{"Key types?"}

    B -- "Only string or symbol" --> C{"Fixed config<br/>or JSON serialization?"}
    B -- "Any type including objects" --> D{"Need automatic<br/>garbage collection?"}

    C -- "Yes" --> E["Plain Object<br/>key and value"]
    C -- "No, dynamic data" --> F["Map with string keys"]

    D -- "Yes, data tied to object lifecycle" --> G["WeakMap"]
    D -- "No, needs iteration" --> H["Map with object keys"]

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

Summary #

  • Always declare type parameters Map<K, V> explicitly — new Map<string, number>() is far more type-safe than new Map() which produces Map<unknown, unknown>.
  • get() returns T | undefined — always check with has() first or use ?? defaultValue before using the returned value.
  • The forEach parameter order is reversed from for...offorEach((value, key) => ...) differs from for (const [key, value] of map) — don’t mix them up.
  • Use Map for dynamic data (registries, caches, counters); use plain objects for static config known at compile time and needing JSON serialization.
  • Map can’t be directly JSON.stringify’d — convert to a plain object with Object.fromEntries(map) before serialization.
  • Object keys in Map are compared by reference, not value — map.get({ id: 1 }) won’t find an entry set with map.set({ id: 1 }, ...) because they’re different objects.
  • WeakMap for data tied to an object’s lifecycle — if you’re storing metadata for DOM elements or class instances and don’t want memory leaks, WeakMap is the right choice because entries are cleaned up automatically when the key is no longer referenced.
  • The frequency counter pattern map.set(k, (map.get(k) ?? 0) + 1) is a very common idiom — use nullish coalescing for safe default values.
  • groupBy with Map produces a more efficient structure than nested objects because iteration is faster and the types are more expressive.

← Previous: List   Next: Date & Time →

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