Mapped Types

Membangun transformer tipe atas bentuk objek dengan mapped types.

Daftar isi

Mapped type adalah tipe yang dibangun dengan memetakan setiap kunci dari sebuah tipe sumber ke tipe nilai baru, sehingga seluruh bentuk objek dapat ditransformasi dalam satu deklarasi. Sintaksnya menyerupai perulangan pada posisi tipe, yaitu [K in keyof T], dan setiap kunci K dipetakan ke tipe hasil yang ditentukan. Topik ini membahas sintaks dasarnya, modifier yang dapat diterapkan, serta penyaringan kunci melalui pemetaan ulang.

Sintaks dasar pemetaan kunci#

Bagian [K in keyof T] mengiterasi seluruh kunci milik T, dan tiap kunci tersebut dipetakan ke tipe yang ditulis di sebelah kanan tanda titik dua. Tipe nilai aslinya dapat dibaca melalui akses terindeks T[K], sehingga rumus pemetaan tetap mengikuti bentuk data yang dipetakan. Hasilnya adalah objek baru dengan kunci yang sama dan tipe nilai yang sudah ditransformasi.

ts transformasi-aset.ts
type Expect<T extends true> = T;
type Equal<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? true : false;

type Dibungkus<T> = { [K in keyof T]: { nilai: T[K] } };

type Catatan = { suhu: number; status: string };

type Hasil = Dibungkus<Catatan>;

const bukti: Expect<Equal<Hasil, { suhu: { nilai: number }; status: { nilai: string } }>> = true;

const laporan: Hasil = {
  suhu: { nilai: 24 },
  status: { nilai: "stabil" },
};

console.log(laporan.suhu.nilai, laporan.status.nilai, bukti);
text Keluaran
24 stabil true

Tipe Dibungkus memetakan setiap properti Catatan menjadi objek pembungkus dengan properti nilai. Bukti Equal menyatakan hasilnya setara dengan { suhu: { nilai: number }; status: { nilai: string } }, yang menunjukkan bahwa kunci dan tipe nilai sumber diperhitungkan satu per satu oleh compiler. Seluruh proses ini terjadi saat kompilasi, sehingga laporan yang ditulis di kode adalah objek biasa tanpa lapisan tambahan apa pun saat program berjalan. Banyak alat bantu bawaan TypeScript dibangun dengan pola semacam ini, sebab Partial, Readonly, dan Pick semuanya didefinisikan sebagai mapped type di atas tipe apa pun yang diberikan kepadanya. Menguasai bentuk dasarnya berarti dapat membaca definisi alat bantu tersebut tanpa menghafal perilakunya satu per satu.

Pemetaan tidak harus dimulai dari keyof T, karena union tipe literal pun dapat diiterasi langsung. Bentuk { [F in "jam" | "hari"]: number } menghasilkan objek dengan kedua kunci tersebut, dan utilitas Record menjadi singkatan ringkas untuk pola yang sama. Selisih keduanya ada pada sumber kunci, yaitu keyof T mengikuti tipe yang sudah ada, sedangkan union literal menetapkan kuncinya sendiri.

Modifier pada hasil pemetaan#

Setiap kunci hasil pemetaan dapat diberi modifier ? untuk sifat opsional atau readonly untuk sifat terkunci, cukup dengan meletakkannya pada kurung pemetaan. Kedua modifier itu juga dapat dilepas dengan tanda minus, yaitu -? dan -readonly, sehingga transformasi dapat dilakukan ke dua arah.

ts draf-pengukuran.ts
type Draf<T> = { [K in keyof T]?: T[K] };
type Terkunci<T> = { readonly [K in keyof T]: T[K] };

type Pengukuran = { titik: string; suhu: number };

const draf: Draf<Pengukuran> = { titik: "A2" };
const terkunci: Terkunci<Pengukuran> = { titik: "A2", suhu: 24 };

console.log(draf.titik, draf.suhu, terkunci.suhu);
text Keluaran
A2 undefined 24

Tipe Draf menjadikan kedua properti opsional, sehingga objek draf sah dideklarasikan hanya dengan titik. Akibatnya draf.suhu bertipe number | undefined, dan keluarannya menunjukkan undefined karena properti itu memang tidak diisi. Tipe Terkunci berkebalikan, yaitu seluruh properti diwariskan dengan sifat readonly, sehingga penugasan ulang pada terkunci.titik ditolak compiler.

Pengisian data bertahap semacam ini lazim pada formulir yang belum lengkap, sedangkan versi terkuncinya cocok untuk data yang sudah lengkap. Karena keduanya diturunkan dari Pengukuran, perubahan pada tipe sumber langsung mengalir ke kedua varian tanpa penyuntingan tambahan. Kedua bentuk tersebut juga dapat dirangkai, misalnya data yang dikumpulkan sebagai draf lalu dikunci setelah diverifikasi lengkap.

Menyaring kunci dengan pemetaan ulang#

Kata kunci as pada kurung pemetaan mengganti nama kunci hasil pemetaan, dan pemakaian yang paling berguna adalah mengganti nama berdasarkan syarat. Bila syarat dipenuhi, kunci dipertahankan atau diubah namanya, sedangkan kunci yang dipetakan ke never dihilangkan dari hasil sama sekali. Dengan begitu, mapped type tidak hanya mengubah nilai, tetapi juga memilih kunci mana yang lolos.

ts bagian-angka.ts
type Expect<T extends true> = T;
type Equal<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? true : false;

type BagianAngka<T> = { [K in keyof T as T[K] extends number ? K : never]: T[K] };

type Pengukuran = { titik: string; suhu: number; kelembapan: number };

type Ukuran = BagianAngka<Pengukuran>;

const bukti: Expect<Equal<Ukuran, { suhu: number; kelembapan: number }>> = true;

const data: Ukuran = { suhu: 24, kelembapan: 61 };

console.log(data.suhu, data.kelembapan, bukti);
text Keluaran
24 61 true

Ekspresi T[K] extends number ? K : never menguji tipe nilai setiap kunci, yaitu tipe bersyarat yang memilih K bila nilainya angka dan never bila bukan. Kunci titik bertipe string, sehingga dipetakan menjadi never, dan hasil Ukuran hanya memuat suhu dan kelembapan, sebagaimana dibuktikan Equal. Objek data wajib memuat kedua kunci tersebut, karena kunci yang lolos saringan tetap bersifat wajib sebagaimana pada tipe sumbernya.

Kesalahan umum#

Kekeliruan pertama adalah menganggap mapped type memproses data saat program berjalan, misalnya membayangkan Dibungkus melingkupi nilai dengan objek pembungkus sungguhan. Seluruh konstruksinya hilang setelah kompilasi, sehingga transformasi yang memang dibutuhkan saat runtime harus ditulis sebagai fungsi biasa. Mapped type hanya menjamin bentuk data yang diizinkan compiler.

Kekeliruan yang sering menyertai pemakaian hasilnya adalah salah membaca bentuk transformasi, misalnya mengisi nilai tanpa objek pembungkusnya. Berkas berikut menuliskan teks "panas" langsung pada properti nilai milik suhu, padahal hasil pemetaan menuntut angka di tempat itu.

ts mapped-salah.ts
// tidak-dijalankan
type Dibungkus<T> = { [K in keyof T]: { nilai: T[K] } };

type Catatan = { suhu: number; status: string };

const d: Dibungkus<Catatan> = {
  suhu: { nilai: "panas" },
  status: { nilai: "ok" },
};

console.log(d);
bash
# tipe nilai pada hasil pemetaan tidak sesuai
tsc --noEmit --pretty false mapped-salah.ts
text Keluaran
mapped-salah.ts(7,11): error TS2322: Type 'string' is not assignable to type 'number'.

Pesan error menunjuk nilai tersebut karena properti nilai pada hasil pemetaan suhu wajib bertipe number, sesuai tipe aslinya pada Catatan. Perbaikannya adalah mengikuti bentuk hasil pemetaan secara persis, yaitu mengisi setiap properti dengan objek pembungkus yang memuat nilai bertipe sumbernya.

Kekeliruan kedua adalah lupa memberi tanda minus ketika modifier perlu dilepas. Menulis ulang properti tanpa ? pada pemetaan tidak menghapus sifat opsional yang dibawa dari tipe sumber, sedangkan bentuk -? memang dirancang untuk menghapusnya. Kekeliruan ketiga adalah memperlakukan kunci hasil pemetaan ulang sebagai kunci tetap. Daftar kuncinya ikut berubah mengikuti syarat as, sehingga objek yang mengisi hasil pemetaan harus mengikuti kunci yang lolos.

Ringkasan#

  • Mapped type mengiterasi kunci sebuah tipe dengan [K in keyof T] dan memetakan setiap kunci ke tipe baru yang dapat membaca T[K].
  • Modifier ? dan readonly dapat diterapkan atau dilepas dengan -? dan -readonly pada setiap kunci hasil pemetaan.
  • Pemetaan ulang dengan as mengganti nama kunci, dan kunci yang dipetakan ke never dihilangkan dari hasil.
  • Seluruh prosesnya berlangsung saat kompilasi, sehingga mapped type mengubah bentuk tipe tanpa meninggalkan jejak pada program yang berjalan.

Referensi#