Conditional Types
Tipe yang bercabang: T extends U ? X : Y, distributive conditional types, dan utility buatan sendiri.
Kenapa Conditional Types Ada?
Kadang tipe output harus BERGANTUNG pada tipe input. Tanpa conditional types, kamu terpaksa menulis overload berulang atau menyerah memakai any.
Conditional type memberi kemampuan bercabang di level tipe: T extends U ? X : Y, artinya "jika T assignable ke U hasilnya X, kalau tidak Y". Logika di dalam tipe inilah fondasi semua utility bawaan seperti Exclude, Extract, dan ReturnType.
Bentuk Dasar: T extends U ? X : Y
type AdalahString<T> = T extends string ? true : false;
type A = AdalahString<"halo">; // true
type B = AdalahString<42>; // false
// Return type mengikuti tipe input
function proses<T>(nilai: T): T extends string ? string[] : number[] {
return (typeof nilai === "string" ? [nilai] : [0]) as T extends string ? string[] : number[];
}
const r1 = proses("x"); // string[]
const r2 = proses(1); // number[]Pemanggil dapat tipe presisi tanpa overload: compiler mengevaluasi cabang saat T diketahui di call site.
Contoh Buatan Sendiri: Flatten dan UnwrapPromise
Kekuatan sebenarnya muncul bersama infer, yang "menangkap" bagian tipe yang cocok pola:
// Ambil tipe elemen array; kalau bukan array, biarkan utuh
type Flatten<T> = T extends (infer E)[] ? E : T;
type F1 = Flatten<string[]>; // string
type F2 = Flatten<number>; // number (bukan array, lolos utuh)
// Buka Promise tanpa memakai Awaited bawaan
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type U1 = UnwrapPromise<Promise<string>>; // string
type U2 = UnwrapPromise<number>; // numberinfer E seperti pattern matching: "kalau T array dari sesuatu, tangkap sesuatu itu sebagai E". Pola ini dipakai ReturnType dan Parameters di balik layar.
Distributive Conditional Types
Konsep kunci: bila yang dicek adalah parameter generik "telanjang" (T langsung) dan T berupa union, conditional type TERDISTRIBUSI ke tiap anggota, lalu hasilnya digabung:
type SaringString<T> = T extends string ? T : never;
type Hasil = SaringString<"a" | 1 | "b" | true>;
// Per anggota: "a" -> "a", 1 -> never, "b" -> "b", true -> never
// Gabungan: "a" | "b"never otomatis hilang dari union, jadi cabang false yang mengembalikan never berfungsi sebagai filter. Inilah definisi asli Exclude:
// Definisi bawaan (disederhanakan):
// type Exclude<T, U> = T extends U ? never : T;
type Coba = Exclude<"a" | 1 | "b", string>; // 1Jelas kenapa Exclude bekerja per anggota: ia memang conditional type distributif. Satu mekanisme ini menjelaskan perilaku tiga modul sebelumnya sekaligus.
Untuk MEMATIKAN distribusi, bungkus kedua sisi dengan tuple:
type SemuaString<T> = [T] extends [string] ? true : false;
type D1 = SemuaString<"a" | "b">; // true (dicek sekaligus)
type D2 = SemuaString<"a" | 1>; // falseContoh Error Nyata: Distribusi yang Tidak Disadari
Jebakan klasik: mengira conditional mengecek union sekaligus, padahal ia terdistribusi:
type KeBoolean<T> = T extends string ? true : false;
type R = KeBoolean<string | number>;
// BUKAN false! Terdistribusi: (string -> true) | (number -> false) = boolean
const boleh: R = true; // OK, R adalah boolean
const salah: KeBoolean<number> = true;
// Error: Type 'true' is not assignable to type 'false'.Baris terakhir gagal karena KeBoolean<number> memang false, dan true tidak assignable ke false. Pelajarannya: distribusi membuat KeBoolean<string | number> menjadi boolean yang longgar; untuk cek "apakah SELURUH union adalah string", pakai pola [T] extends [string].
Catatan teknis: Distribusi hanya terjadi bila yang dicek adalah naked type parameter.
T[] extends Uatau[T] extends [U]mematikan distribusi karenaTtidak lagi telanjang. Aturan kecil ini menjelaskan banyak perilaku "aneh" utility bawaan, dan menguasainya membedakan pemakai dari perancang tipe.
Tantangan
NonNullable buatan sendiri
Tulis conditional type distributif TanpaNull<T> = T extends null | undefined ? never : T (NonNullable versi manual). Uji: TanpaNull<string | null> harus string, TanpaNull<number | undefined> harus number. Lalu tulis fungsi generik bersihkan<T>(arr: (T | null | undefined)[]): TanpaNull<T>[] yang membuang nilai null/undefined memakai type predicate.