Exclude dan Extract
Filter union type dengan Exclude, Extract, dan NonNullable: satu sumber kebenaran untuk varian tipe.
Kenapa Exclude dan Extract Ada?
Bayangkan dashboard dengan daftar event: "bacaPost", "tulisKomentar" untuk user biasa, plus "blokirUser", "hapusPost", "pinPost" khusus admin. Tanpa utility types, kamu menulis dua union terpisah dan menjaganya tetap sinkron secara manual. Saat backend menambah event baru, kamu harus ingat mengupdate kedua tempat, dan manusia pasti lupa di satu tempat.
Exclude<T, U> dan Extract<T, U> menyelesaikan ini dengan cara derivasi: kamu cukup punya SATU union sumber, lalu varian-variannya didefinisikan sebagai filter dari sumber itu. Tambah event baru di sumber, semua varian ikut berubah otomatis. Ini pola yang sama seperti Pick dan Omit untuk object, tapi bekerja di level union.
Exclude: Membuang Anggota Union
Exclude<T, U> menghasilkan union berisi anggota T yang TIDAK bisa diassign ke U. Contoh pemisahan event admin vs user:
type Event =
| "bacaPost"
| "tulisKomentar"
| "blokirUser"
| "hapusPost"
| "pinPost";
type EventAdmin = Exclude<Event, "bacaPost" | "tulisKomentar">;
// "blokirUser" | "hapusPost" | "pinPost"
type EventUser = Exclude<Event, EventAdmin>;
// "bacaPost" | "tulisKomentar"
function kirimNotifikasiAdmin(e: EventAdmin): void {
console.log("Notifikasi admin:", e);
}
kirimNotifikasiAdmin("hapusPost"); // OK
kirimNotifikasiAdmin("pinPost"); // OK, event baru otomatis masukPerhatikan baris terakhir: saat seseorang menambah "pinPost" ke Event, EventAdmin langsung mencakupnya tanpa diubah. Itulah nilai derivasi dibanding menulis union manual.
Extract: Menyimpan yang Cocok Saja
Extract<T, U> adalah kebalikannya: ia menyimpan anggota T yang BISA diassign ke U. Cocok untuk pola whitelist, misalnya hanya event tertentu yang boleh dicatat ke analitik:
type EventAnalitik = Extract<Event, "bacaPost" | "pinPost" | "arsipPost">;
// "bacaPost" | "pinPost"
// "arsipPost" tidak ada di Event, jadi hilang begitu saja
function catatAnalitik(e: EventAnalitik): void {
console.log("Analitik:", e);
}
catatAnalitik("bacaPost"); // OKExclude<T, U> dan Extract<T, U> saling melengkapi: setiap anggota T pasti masuk ke salah satu dari keduanya, tidak ada yang hilang dan tidak ada yang ganda. Pilih Exclude bila yang dibuang sedikit (blacklist), pilih Extract bila yang disimpan sedikit (whitelist).
NonNullable: Membuang null dan undefined
Kasus khusus yang sangat sering muncul: hasil fungsi boleh null, tapi di satu titik kamu sudah yakin nilainya ada. NonNullable<T> tidak lain adalah Exclude<T, null | undefined>:
function cariUser(id: string): { nama: string } | null {
// ... cari di database, bisa tidak ketemu
return null;
}
type UserPastiAda = NonNullable<ReturnType<typeof cariUser>>;
// { nama: string }
const hasil = cariUser("u-1");
if (hasil !== null) {
const user: UserPastiAda = hasil; // OK di dalam guard
console.log(user.nama);
}Kombinasi NonNullable<ReturnType<typeof fn>> adalah idiom standar untuk mengambil "tipe sukses" dari fungsi yang bisa gagal, tanpa menulis ulang bentuk objectnya.
Contoh Error Nyata: Tipe Hasil Filter Lebih Sempit
Jebakan umum: tipe hasil filter adalah SUBSET dari sumber, jadi nilai bertipe sumber tidak otomatis bisa dipakai di tempat yang meminta hasil filter:
declare const sembarangEvent: Event;
function butuhAdmin(e: EventAdmin): void {
console.log(e);
}
butuhAdmin(sembarangEvent);
// Error: Argument of type 'Event' is not assignable to
// parameter of type 'EventAdmin'.
// Type '"bacaPost"' is not assignable to type 'EventAdmin'.Pesan errornya jujur: Event masih memuat "bacaPost" yang bukan event admin. Perbaikannya adalah narrowing dulu sebelum memanggil, misalnya cek if (sembarangEvent === "blokirUser" || sembarangEvent === "hapusPost" || sembarangEvent === "pinPost"), atau ubah desain supaya fungsi menerima Event lalu memfilter di dalam. Compiler tidak menebak maksudmu, ia memaksamu eksplisit.
Catatan teknis:
ExcludedanExtractbekerja per anggota union (distributif), bukan ke union sebagai satu kesatuan.Exclude<A | B, X>sama denganExclude<A, X> | Exclude<B, X>. Kalau tidak ada anggota yang lolos filter, hasilnya adalahnever, tipe kosong yang tidak bisa diisi nilai apa pun. Mekanisme distributif inilah yang akan dibahas tuntas di modul conditional types.
Tantangan
Filter status pesanan
Diberi type Status = "draft" | "review" | "published" | "archived" | "deleted". Definisikan StatusAktif = Exclude<Status, "archived" | "deleted"> dan StatusButuhAksi = Extract<Status, "draft" | "review">. Lalu tulis fungsi tandaiButuhAksi(s: StatusButuhAksi) dan buktikan compiler menolak tandaiButuhAksi("published") (tulis pemanggilan yang salah sebagai komentar beserta pesan errornya).