Conditional type adalah tipe yang memilih salah satu hasilnya berdasarkan syarat, ditulis T extends U ? X : Y. Bila T memenuhi syarat U, hasilnya X, sedangkan bila tidak, hasilnya Y, sehingga percabangan if kini tersedia di level tipe. Topik ini membahas bentuk dasarnya, sifat distributivitasnya atas union, serta keadaan ketika distributivitas perlu dimatikan.
Bentuk dasar tipe bersyarat#
Ekspresi T extends U ? X : Y dibaca sebagaimana percabangan biasa, yaitu syarat ditempatkan di tengah dan dua hasil dipisahkan tanda titik dua. Syaratnya berupa pemeriksaan kecocokan bentuk melalui extends, dan hasilnya boleh berupa tipe apa pun, termasuk tipe literal atau tipe yang dinyatakan sebelumnya. Seluruh perhitungan dilakukan compiler saat kompilasi, sehingga tidak ada biaya apa pun saat program berjalan.
type Expect<T extends true> = T;
type Equal<X, Y> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? true : false;
type JikaString<T> = T extends string ? true : false;
type Kelas<T> =
T extends number ? "angka" :
T extends string ? "teks" : "lain";
const bukti1: Expect<Equal<JikaString<"arsip">, true>> = true;
const bukti2: Expect<Equal<JikaString<number>, false>> = true;
const bukti3: Expect<Equal<Kelas<40>, "angka">> = true;
const bukti4: Expect<Equal<Kelas<"A2">, "teks">> = true;
const bukti5: Expect<Equal<Kelas<boolean>, "lain">> = true;
console.log(bukti1, bukti2, bukti3, bukti4, bukti5);true true true true trueTipe JikaString memilih true bila argumennya string dan false bila bukan, sedangkan Kelas merangkai dua pemeriksaan menjadi rantai. Rantai semacam ini berperan seperti else if, karena cabang yang tidak cocok meneruskan pemeriksaan ke baris berikutnya sampai ditemukan cabang yang cocok atau sampai hasil akhir lain. Bukti Equal pada kelima baris menyatakan hasil yang diharapkan, dan pemeriksaan tipe gagal seketika bila salah satu dugaan itu keliru.
Bentuk dasar ini menjadi penyusun hampir seluruh alat bantu tipe yang bersyarat, misalnya memilih tipe kembalian berdasarkan bentuk masukan. Syarat extends tidak menuntut kesamaan penuh, melainkan kecocokan struktural, sehingga tipe yang lebih lengkap tetap memenuhi syarat yang lebih longgar. Tipe { titik: string; suhu: number } memenuhi syarat { suhu: number }, karena seluruh properti yang disyaratkan tersedia. Aturan pemeriksaannya pun selaras dengan aturan penugasan yang dipakai compiler di tempat lain.
Perhitungan conditional type juga menunggu hingga argumennya diketahui. Bila parameter tipe masih generik dan belum terisi, compiler menahan hasilnya dalam bentuk tertunda, lalu menyelesaikannya begitu tipe konkret diberikan pada titik pemanggilan. Penundaan ini membuat satu definisi dapat dipakai untuk banyak tipe tanpa mengulang percabangannya.
Distributivitas atas union#
Apabila parameter tipe polos berdiri sendiri pada posisi syarat, conditional type diterapkan pada setiap anggota union satu per satu, lalu hasilnya digabungkan kembali menjadi union. Sifat ini disebut distributivitas, dan penerapannya paling terasa pada pemilihan anggota union. Tipe Pilih di bawah ini menyaring union dan hanya mempertahankan anggota yang bertipe string.
type Expect<T extends true> = T;
type Equal<X, Y> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? true : false;
type Pilih<T> = T extends string ? T : never;
type Sumber = "darat" | 7 | "laut";
type Lolos = Pilih<Sumber>;
type UjiBungkus<T> = [T] extends [string] ? "teks" : "campuran";
type Satuan = UjiBungkus<"darat">;
type Gabungan = UjiBungkus<"darat" | 7>;
const bukti1: Expect<Equal<Lolos, "darat" | "laut">> = true;
const bukti2: Expect<Equal<Satuan, "teks">> = true;
const bukti3: Expect<Equal<Gabungan, "campuran">> = true;
console.log(bukti1, bukti2, bukti3);true true truePemanggilan Pilih<Sumber> bekerja seolah tipe tersebut dipanggil tiga kali, yaitu untuk "darat", untuk 7, dan untuk "laut", kemudian hasilnya dirangkai. Anggota 7 dipetakan menjadi never dan hilang dari union, sehingga Lolos setara dengan "darat" | "laut". Intuisinya, parameter tipe polos pada posisi syarat memperlakukan union sebagai sekumpulan anggota yang diproses terpisah.
Baris berikutnya memperlihatkan cara mematikan perilaku tersebut. Menuliskan parameternya di dalam tanda kurung siku, yaitu [T] extends [string], membuat union diperiksa sebagai satu kesatuan dan bukan per anggota. Karena union "darat" | 7 tidak sepenuhnya bertipe string, hasilnya campuran, padahal versi terdistribusi menjawab teks hanya untuk anggota string dan campuran untuk sisanya. Pilihan di antara keduanya ditentukan oleh makna pertanyaannya, yaitu memeriksa setiap anggota atau memeriksa keseluruhan union sekaligus.
Distributivitas juga berlaku untuk union kosong, yaitu tipe never. Pemanggilan Pilih<never> menghasilkan never, karena tidak ada anggota yang diproses dan tidak ada hasil yang dapat digabungkan. Perilaku ini konsisten dengan intuisi union sebagai sekumpulan anggota, dan sering dipakai secara sengaja untuk menyaring tipe yang tidak relevan.
Kesalahan umum#
Kekeliruan pertama adalah lupa bahwa conditional type atas union menghasilkan union berisi hasil setiap anggotanya, bukan satu nilai tunggal. Berkas berikut menguji union melalui tipe Kelas, lalu menugaskan angka ke variabelnya, dan compiler menampilkan hasil distribusinya pada pesan error.
// tidak-dijalankan
type Kelas<T> = T extends string ? "teks" : "bukan-teks";
const h: Kelas<string | number> = 5;
console.log(h);# berkas ini sengaja tidak lolos pemeriksaan tipe
tsc --noEmit --pretty false distributif-salah.tsdistributif-salah.ts(4,7): error TS2322: Type '5' is not assignable to type '"bukan-teks" | "teks"'.Pesan error menyebut "bukan-teks" | "teks", yang membuktikan bahwa union masukan diproses per anggota sebelum digabungkan kembali. Perbaikan untuk kasus semacam ini bergantung pada maksudnya, yaitu menugaskan salah satu anggota union yang sah atau membungkus parameternya dengan [T] bila yang dimaksudkan adalah pemeriksaan keseluruhan.
Kekeliruan kedua adalah mengira conditional type dihitung saat program berjalan, misalnya mengharapkan percabangan tipe bereaksi terhadap nilai yang baru diketahui pengguna. Seluruh keputusannya terjadi saat kompilasi atas tipe yang tersedia. Data yang bentuknya baru diketahui saat runtime harus melewati pemeriksaan atau penguraian terlebih dahulu sebelum masuk ke tipe yang bersyarat.
Kekeliruan ketiga adalah menyalin pola bersyarat ke tempat yang sebenarnya hanya membutuhkan batasan biasa. Batasan T extends U pada parameter tipe sudah cukup bila tujuannya menjamin bentuk argumen, sedangkan conditional type diperlukan bila hasilnya harus berbeda sesuai syarat. Memilih bentuk yang lebih sederhana menjaga definisi tetap mudah dibaca.
Ringkasan#
- Conditional type
T extends U ? X : YmemilihXbilaTmemenuhi bentukU, danYbila tidak, semuanya dihitung saat kompilasi. - Rantai pemeriksaan berperan seperti
else ifpada level tipe dan menjadi penyusun alat bantu tipe yang bersyarat. - Parameter tipe polos pada posisi syarat terdistribusi atas union, sehingga setiap anggota diproses terpisah lalu digabungkan kembali.
- Pembungkusan
[T] extends [U]memeriksa union sebagai satu kesatuan dan mematikan distributivitas.
Referensi#
- Conditional Types — The TypeScript Handbook — Sumber utama bentuk dasar, distributivitas, dan contoh pemakaiannya.
- Utility Types — The TypeScript Handbook — Menampilkan alat bantu bawaan yang implementasinya memakai conditional types.