utility-typesunionMenengah4 mnt baca

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:

typescript
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 masuk

Perhatikan 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:

typescript
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"); // OK

Exclude<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>:

typescript
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:

typescript
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: Exclude dan Extract bekerja per anggota union (distributif), bukan ke union sebagai satu kesatuan. Exclude<A | B, X> sama dengan Exclude<A, X> | Exclude<B, X>. Kalau tidak ada anggota yang lolos filter, hasilnya adalah never, 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).