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 betweenfor...ofandforEach: withfor...of, destructuring yields[key, value]; but withforEach, 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:
| Aspect | Map<K, V> | Plain Object {} |
|---|---|---|
| Key types | All types | string, number, Symbol |
| Key order | Guaranteed — insertion order | Not fully guaranteed |
size property | ✓ map.size | ✗ Needs Object.keys(obj).length |
| Iteration | Direct with for...of | Needs Object.entries() |
| Insert/delete performance | Better for dynamic data | Better for fixed structures |
| JSON serialization | ✗ JSON.stringify(map) → {} | ✓ Direct JSON.stringify(obj) |
| Prototype pollution | ✓ No risk | ✗ Risk (key names can collide) |
| Use cases | Cache, registry, dynamic data | Config, 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:#fffSummary #
- Always declare type parameters
Map<K, V>explicitly —new Map<string, number>()is far more type-safe thannew Map()which producesMap<unknown, unknown>.get()returnsT | undefined— always check withhas()first or use?? defaultValuebefore using the returned value.- The
forEachparameter order is reversed fromfor...of—forEach((value, key) => ...)differs fromfor (const [key, value] of map)— don’t mix them up.- Use
Mapfor dynamic data (registries, caches, counters); use plain objects for static config known at compile time and needing JSON serialization.Mapcan’t be directlyJSON.stringify’d — convert to a plain object withObject.fromEntries(map)before serialization.- Object keys in
Mapare compared by reference, not value —map.get({ id: 1 })won’t find an entry set withmap.set({ id: 1 }, ...)because they’re different objects.WeakMapfor data tied to an object’s lifecycle — if you’re storing metadata for DOM elements or class instances and don’t want memory leaks,WeakMapis 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.groupBywithMapproduces a more efficient structure than nested objects because iteration is faster and the types are more expressive.