Bayangkan kamu sudah mendefinisikan bentuk sebuah User di kode — ada nama, email, umur, lengkap. Lalu kamu butuh versi yang sedikit berbeda: satu versi di mana semua field boleh dikosongkan (untuk draf formulir), atau versi yang hanya menyimpan nama dan email, atau versi yang isinya tidak boleh diubah lagi setelah dibuat. Naluri pertama biasanya menyalin tipe aslinya lalu mengeditnya manual. Cara itu jalan sekali. Tapi begitu tipe aslinya berubah, salinanmu tidak ikut berubah, dan keduanya pelan-pelan jadi tidak cocok.
TypeScript punya jalan yang lebih rapi. Ia membawa sekumpulan utility type — pembantu bawaan yang menerima sebuah tipe lalu mengembalikan versi transformasinya. Kamu tidak menulis ulang bentuknya; kamu cukup mendeskripsikan perubahannya. Ketika tipe asli berubah, semua tipe turunannya ikut diperbarui otomatis. Artikel ini membahas yang paling sering kamu pakai, apa fungsi masing-masing, dan kenapa semua ini menghemat banyak pekerjaan berulang.
Apa itu utility type sebenarnya
Utility type pada dasarnya adalah tipe yang menerima tipe lain sebagai masukan, lalu menghasilkan tipe baru sebagai keluaran. Anggap saja mirip fungsi, hanya saja ia bekerja pada tipe saat kompilasi, bukan pada nilai saat program berjalan. Masukannya ditaruh di dalam kurung sudut — <T> yang sering kamu lihat itu adalah parameter tipe, sebuah penampung untuk “tipe apa pun yang kamu masukkan.”
Berikut tipe User yang akan kita pakai berulang kali:
interface User {
id: number;
name: string;
email: string;
age: number;
}
Semua contoh di bawah mentransformasi bentuk yang satu ini. Tidak ada yang membuatnya ulang.
Partial: jadikan semua field opsional
Partial<T> mengambil sebuah tipe lalu menandai setiap propertinya sebagai opsional. Ini pas sekali untuk fungsi “update”, saat pemanggil hanya ingin mengubah satu atau dua field saja.
function updateUser(id: number, changes: Partial<User>) {
// changes boleh { name: "Jane Doe" } — atau { age: 30, email: "..." }
// semua field boleh, tapi tidak ada yang wajib
}
updateUser(1, { name: "Jane Doe" }); // valid
updateUser(2, {}); // juga valid
Tanpa Partial, kamu harus menulis satu interface kedua dengan tanda tanya di setiap field, lalu menjaganya tetap sinkron selamanya. Dengan Partial, hubungannya jadi permanen: tambah field baru ke User, maka Partial<User> otomatis dapat field opsional itu juga.
Required: kebalikan persisnya
Required<T> melakukan hal sebaliknya — ia mencabut penanda opsional dan memaksa setiap properti harus ada. Ini berguna ketika sebuah nilai datang dalam keadaan longgar (banyak field opsional), tapi kamu sudah memvalidasinya dan sekarang ingin memastikan semuanya terisi.
interface Settings {
theme?: string;
fontSize?: number;
}
function applySettings(settings: Required<Settings>) {
// di sini, theme dan fontSize dijamin sama-sama ada
console.log(settings.theme.toUpperCase());
}
Readonly: kunci supaya tak bisa diubah
Readonly<T> mempertahankan bentuk yang sama, tapi menandai setiap properti sebagai hanya-baca, sehingga kompiler menolak segala upaya mengubah field setelah objeknya dibuat. Pakai ini saat kamu ingin mengoper data ke mana-mana tanpa ada yang tidak sengaja mengubahnya.
const user: Readonly<User> = {
id: 1, name: "John Doe", email: "john@example.com", age: 28,
};
user.name = "Diganti"; // Error: tidak bisa menetapkan 'name', sifatnya read-only
Ini jaminan saat kompilasi
Readonly diperiksa oleh TypeScript ketika kamu menulis kode, bukan dipaksakan saat program berjalan. JavaScript hasil kompilasinya tidak tahu bahwa properti itu seharusnya dibekukan. Kalau kamu butuh kunci sungguhan saat runtime, gunakan juga Object.freeze().
Pick: ambil hanya field yang kamu mau
Pick<T, Keys> membentuk tipe baru dari tipe yang sudah ada dengan memilih sebagian propertinya. Kamu memberi tipe sumbernya dan daftar key yang ingin dipertahankan.
type UserPreview = Pick<User, "id" | "name">;
// setara dengan: { id: number; name: string }
const preview: UserPreview = { id: 1, name: "John Doe" };
Ini cocok untuk tampilan daftar atau respons API yang hanya butuh sepotong dari objek lengkap. Tipe setiap field ikut terbawa otomatis — id tetap number, name tetap string — jadi tidak ada yang perlu disalin.
Omit: pertahankan semua kecuali beberapa field
Omit<T, Keys> adalah cerminan dari Pick. Alih-alih menyebut field mana yang dipertahankan, kamu menyebut mana yang dibuang, lalu sisanya kamu dapatkan.
type PublicUser = Omit<User, "email" | "age">;
// setara dengan: { id: number; name: string }
const publicData: PublicUser = { id: 1, name: "Jane Doe" };
Kapan pakai Pick, kapan pakai Omit? Aturan sederhananya: kalau kamu mempertahankan sebagian besar objek dan hanya membuang satu-dua field, pakai Omit. Kalau kamu cuma menyimpan beberapa field dari sekian banyak, pakai Pick. Keduanya menghasilkan jenis hasil yang sama; kamu tinggal memilih deskripsi yang lebih singkat.
Record: bangun tipe objek dari key dan value
Record<Keys, ValueType> menyusun tipe objek yang key-nya bertipe tertentu dan setiap value-nya bertipe lain. Ini cara paling bersih untuk mendeskripsikan “tabel pencarian” atau “kamus.”
type Role = "admin" | "editor" | "viewer";
const permissions: Record<Role, boolean> = {
admin: true,
editor: true,
viewer: false,
};
Karena Role hanya mengizinkan tiga string tertentu, TypeScript akan protes kalau kamu melewatkan salah satunya atau menambahkan key yang bukan role yang sah. Kamu mendapat tabel yang lengkap dan terperiksa, bukan objek yang asal-asalan.
Exclude: buang anggota dari sebuah union
Pembantu sebelumnya mengubah bentuk tipe objek. Exclude<T, U> bekerja pada tipe union — tipe “ini atau itu atau yang lain” yang dirangkai dengan |. Ia membuang dari T setiap anggota yang juga ada di U.
type Status = "draft" | "published" | "archived";
type ActiveStatus = Exclude<Status, "archived">;
// hasil: "draft" | "published"
Ini berguna saat kamu punya daftar utama berisi pilihan dan ingin menurunkan daftar yang lebih sempit darinya, tanpa menulis ulang daftar sempit itu satu per satu. (Ada saudaranya, Extract<T, U>, yang melakukan kebalikannya — ia hanya menyimpan anggota yang ada di U.)
ReturnType: ambil tipe keluaran sebuah fungsi
ReturnType<T> menjangkau ke dalam sebuah tipe fungsi lalu menarik keluar tipe dari apa pun yang dikembalikan fungsi itu. Kamu jarang mau mengetik bentuk kembalian yang rumit dua kali, jadi ini memungkinkanmu merujuknya saja.
function createUser(name: string, email: string) {
return { id: Date.now(), name, email, active: true };
}
type NewUser = ReturnType<typeof createUser>;
// { id: number; name: string; email: string; active: boolean }
Perhatikan bagian typeof createUser. ReturnType mengharapkan sebuah tipe fungsi, bukan nilai fungsi, jadi typeof yang mengubah fungsi aslinya menjadi tipenya. Hasilnya, kalau nanti kamu menambah field ke objek yang dikembalikan, NewUser akan menyesuaikan sendiri.
Semua ini bisa digabung
Utility type bisa ditumpuk. Mau tipe yang hanya punya id dan name, tapi keduanya opsional? Tulis saja Partial<Pick<User, "id" | "name">>. Kamu bisa menyusunnya sedalam kebutuhan — setiap utility cukup mengoper hasilnya ke utility berikutnya.
Tabel rujukan singkat
| Utility | Fungsinya | Contoh kecil |
|---|---|---|
Partial<T> |
Membuat semua properti jadi opsional | Partial<User> |
Required<T> |
Membuat semua properti jadi wajib | Required<Settings> |
Readonly<T> |
Membuat semua properti jadi hanya-baca | Readonly<User> |
Pick<T, K> |
Menyimpan hanya key yang disebut | Pick<User, "id" | "name"> |
Omit<T, K> |
Membuang key yang disebut | Omit<User, "email"> |
Record<K, V> |
Membangun tipe objek dari key + tipe value | Record<Role, boolean> |
Exclude<T, U> |
Membuang anggota union yang ada di U |
Exclude<Status, "archived"> |
ReturnType<T> |
Mengambil tipe kembalian sebuah fungsi | ReturnType<typeof createUser> |
Kenapa ini penting dalam praktik
Benang merah dari semua ini adalah satu sumber kebenaran. Kamu mendefinisikan sebuah bentuk sekali, lalu setiap variasinya diturunkan dari definisi itu, bukan disalin. Ketika kebutuhan berubah — ada field baru, properti diganti nama, nilai kembalian berbeda — kamu cukup mengedit satu tempat dan semua yang bergantung padanya ikut menyesuaikan. Itulah beda antara sistem tipe yang membantumu dan yang malah terus kamu lawan supaya tetap sinkron.
Ada manfaat kedua yang mudah terlewat: keterbacaan. Omit<User, "password"> langsung memberi tahu orang berikutnya yang membaca kodemu apa yang sedang terjadi — “ini User, dikurangi password.” Sebuah interface buatan tangan yang menyalin sebagian besar field User tidak menjelaskan maksud itu sama sekali. Utility type membuat transformasimu menjelaskan dirinya sendiri.
Rangkuman
Utility type TypeScript memungkinkanmu mentransformasi tipe yang sudah ada, bukan menulisnya ulang:
PartialdanRequiredmembalik status opsional setiap field, on atau off.Readonlymencegah perubahan setelah objek dibuat (saat kompilasi).PickdanOmitmemotong sebagian dari tipe objek — yang satu menyimpan, yang lain membuang.Recordmembangun tipe objek key-ke-value yang terperiksa, ideal untuk tabel pencarian.Excludememangkas anggota dari sebuah union (denganExtractsebagai kembarannya yang menyimpan).ReturnTypemengangkat tipe kembalian sebuah fungsi supaya kamu tak perlu mendeskripsikannya dua kali.
Pakai semua ini begitu kamu sadar hampir menyalin sebuah tipe lalu mengutak-atiknya. Deskripsikan transformasinya saja, biar TypeScript yang menjaga semua bagiannya tetap selaras.