Record<K, V> adalah utility type bawaan untuk object yang key-nya bertipe K dan semua nilainya bertipe V. Dengan key string, tipe ini menggambarkan dictionary; dengan union dari literal key, tipe ini menggambarkan object yang harus punya tepat key-key itu.
Record hanya ada di sistem tipe. Saat runtime, kedua object itu adalah object JavaScript biasa, jadi keduanya bekerja dengan object literal, spread, JSON.stringify, dan apa pun yang menerima object.
Sintaks dan Definisi
Record<Keys, Value>
Keys harus sesuatu yang bisa menjadi key object: string, number, symbol, union dari literal string atau number, atau template literal type. Value boleh tipe apa pun. Seluruh definisinya di standard library TypeScript hanya satu baris, sebuah mapped type:
type Record<K extends keyof any, T> = {
[P in K]: T;
};
keyof any adalah string | number | symbol, himpunan semua tipe key yang mungkin. [P in K]: T membuat satu property bertipe T untuk setiap anggota K. Itulah yang menjelaskan kedua perilaku di bawah: K yang luas seperti string menghasilkan index signature (key apa pun), dan K berupa union menghasilkan satu property wajib untuk setiap anggotanya.
Key Union: Setiap Key Wajib Ada
Kalau key-nya berupa union dari literal, sebuah Record harus mencantumkan setiap key itu, dan tidak boleh yang lain. Ini mengubah compiler menjadi daftar periksa:
Tambahkan "cancelled" ke Status dan kedua object itu berhenti lolos kompilasi sampai kamu memberi status baru itu label dan warna. Melewatkan sebuah key, atau menambahkan key yang tidak ada di union, adalah error kompilasi:
index.ts(4,7): error TS2741: Property 'error' is missing in type '{ idle: string; loading: string; success: string; }' but required in type 'Record<Status, string>'.
index.ts(11,54): error TS2353: Object literal may only specify known properties, and 'paused' does not exist in type 'Record<Status, string>'.
Hal yang sama berlaku dengan string enum sebagai tipe key: Record<Color, string> mewajibkan satu entri untuk setiap anggota enum.
Record<string, T> dan Key yang Tidak Ada
Dengan key string, key apa pun diperbolehkan, dan TypeScript memberi setiap pembacaan tipe T, bahkan untuk key yang tidak ada. Saat runtime, key yang tidak ada menghasilkan undefined:
Ini adalah bug Record yang paling umum. Ada tiga cara menanganinya: cek dengan in atau Object.hasOwn sebelum membaca, deklarasikan nilainya sebagai V | undefined, atau aktifkan opsi compiler noUncheckedIndexedAccess, yang menambahkan | undefined ke setiap pembacaan index signature di seluruh proyek. Record dengan key union tidak punya masalah ini, karena setiap key dijamin ada.
Partial<Record<K, V>>: Hanya Sebagian Key
Untuk memakai union key tanpa mewajibkan semuanya, bungkus Record dengan Partial. Pembacaan lalu mengembalikan V | undefined, dan itu memang jujur:
Key yang salah eja seperti jp tetap menjadi error, dan itulah keunggulannya dibanding Record<string, string>.
Perulangan atas Record
Object.keys, Object.values dan Object.entries semuanya berfungsi. Masalahnya ada di tipe key: Object.keys mengembalikan string[] dan Object.entries mengembalikan [string, V][], tidak pernah union key milikmu:
TypeScript sengaja mempertahankan key sebagai string: sebuah object bisa punya property lebih banyak saat runtime daripada yang tercantum di tipenya, jadi menjanjikan Plan[] secara umum tidak aman. Untuk object yang kamu buat dari literal, seperti seats, cast-nya aman.
Membangun Record dari Data
Record adalah hasil yang biasa dari mengelompokkan atau mengindeks sebuah array. Mulai dari object kosong bertipe Record, lalu isi:
Book["genre"] memakai ulang union dari interface sebagai tipe key, jadi menambahkan genre ke Book membuat byGenre menuntut entri baru.
Record<string, unknown> dan Interface
Record<string, unknown> adalah tipe yang umum untuk "suatu object dengan key string". Tipe ini menerima object literal dan nilai yang bertipe type alias, tetapi interface ditolak:
index.ts(11,11): error TS2345: Argument of type 'User' is not assignable to parameter of type 'Record<string, unknown>'.
Index signature for type 'string' is missing in type 'User'.
Interface bisa diperluas lewat declaration merging, jadi TypeScript tidak menganggapnya cocok dengan index signature; type alias tidak bisa dibuka ulang, jadi type User = { name: string } akan lolos. Perbaikan yang biasa adalah menerima object sebagai gantinya (kamu tetap bisa memanggil Object.keys padanya), membuat fungsinya generic (<T extends object>(obj: T)), atau mendeklarasikan User dengan type (halaman interface vs type membahas perbedaan lainnya).
Record vs Index Signature vs Map
Record<K, V> | { [key: string]: V } | Map<K, V> | |
|---|---|---|---|
| Ada saat runtime | tidak, object biasa | tidak, object biasa | ya, sebuah class |
| Himpunan key yang tetap | ya, dengan K berupa union | tidak | tidak |
| Tipe key | string, number, symbol, union literal, pola template | string, number, symbol, pola template | apa pun, termasuk object |
| Tipe saat membaca key yang tidak ada | V (dengan key string) | V | V | undefined dari get |
| Dicampur dengan property bernama | lewat intersection & | ya, dalam tipe yang sama | tidak |
| JSON dan spread | ya | ya | tidak, konversi dulu |
| Ukuran | Object.keys(r).length | Object.keys(o).length | m.size |
| Sering menambah dan menghapus | bisa | bisa | memang dirancang untuk itu |
Pilih Record dengan key union setiap kali himpunan key-nya diketahui: hanya opsi ini yang mengecek bahwa setiap key ada. Untuk key string yang terbuka, Record<string, V> dan index signature bisa saling menggantikan, dan banyak codebase memilih Record karena lebih mudah dibaca. Pakai Map kalau key ditambah dan dihapus saat runtime, kalau key-nya bukan string, atau kalau kamu butuh ukuran dan urutan penyisipan tanpa kerja tambahan.
Pertanyaan yang sering diajukan
Apa itu Record di TypeScript?
Record<K, V> adalah utility type bawaan untuk object yang key-nya bertipe K dan semua nilainya bertipe V. Record<string, number> adalah object dengan key string apa pun dan nilai number; Record<"en" | "de", string> adalah object dengan tepat key en dan de, keduanya string.
Apa perbedaan Record dan Map di TypeScript?
Record adalah tipe untuk object JavaScript biasa, jadi ia bekerja dengan object literal, JSON, dan spread, lalu menghilang saat kompilasi. Map adalah class runtime dengan get, set, has dan size, menjaga urutan penyisipan untuk semua key, menerima tipe key apa pun (termasuk object), dan get mengembalikan V | undefined. Pakai Record untuk data yang tetap atau berbentuk JSON, dan Map untuk key yang ditambah dan dihapus saat runtime.
Apa perbedaan Record<string, T> dan { [key: string]: T }?
Untuk nilainya, keduanya tipe yang sama: Record<string, T> dijabarkan menjadi object type dengan index signature string. Ada dua perbedaan kecil: index signature bisa diberi nama dan ditaruh di samping property lain dalam tipe yang sama, dan keyof Record<string, T> adalah string, sedangkan keyof { [key: string]: T } adalah string | number.
Bagaimana cara melakukan perulangan atas Record di TypeScript?
Pakai Object.entries(record) untuk pasangan key-nilai, Object.keys untuk key, dan Object.values untuk nilai. Key-nya dikembalikan sebagai string, bukan sebagai K, karena sebuah object bisa menyimpan key tambahan saat runtime. Kalau Record-nya punya union key yang diketahui, lakukan cast: (Object.keys(r) as Array<keyof typeof r>).
Bagaimana cara membuat hanya sebagian key Record yang wajib?
Dengan key union, Record<K, V> mewajibkan setiap key. Bungkus dengan Partial untuk membuat semuanya opsional: Partial<Record<Lang, string>>. Untuk campuran, pakai intersection: Record<"en", string> & Partial<Record<"de" | "fr", string>> mewajibkan en dan mengizinkan yang lain.