Conditional type memilih di antara dua tipe dengan sebuah tes yang bentuknya mirip ternary JavaScript: T extends U ? X : Y. Kalau T bisa diberikan ke U, hasilnya X; kalau tidak, hasilnya Y.
extends di sini berarti "bisa diberikan ke", hubungan yang sama yang dipakai compiler saat kamu memberikan nilai ke sebuah variabel. Conditional type hanya ada saat kompilasi; tipe ini dihapus dari output JavaScript.
Sintaksnya
type Result = CheckedType extends TestType ? TrueType : FalseType;
Conditional type menjadi berguna bersama generic, di mana tipe yang diperiksa adalah type parameter yang baru mendapat tipe konkret belakangan. Di cabang true, TypeScript tahu bahwa tipe yang diperiksa cocok dengan tesnya, jadi T["message"] di atas diizinkan meskipun T polos tidak punya property message.
Conditional type juga bisa bersarang, seperti ternary yang dirangkai:
as TypeName<T> di badan fungsi itu tidak opsional, seperti yang ditunjukkan bagian berikutnya.
Tipe Kembalian Kondisional Butuh Assertion
Fungsi yang tipe kembaliannya adalah conditional type atas type parameter miliknya sendiri tidak bisa langsung mengembalikan salah satu cabang. TypeScript tidak mempersempit T di dalam badan fungsi, jadi ia tidak bisa tahu cabang mana yang berlaku:
index.ts(5,34): error TS2322: Type 'number' is not assignable to type 'Flip<T>'.
index.ts(5,45): error TS2322: Type 'string' is not assignable to type 'Flip<T>'.
Ada dua perbaikan yang umum: overload, yang menyatakan setiap pasangan input dan output serta memeriksa pemanggil dengan tepat, atau assertion di implementasinya.
Overload dibahas di halaman function overloading. Dengan assertion, compiler memercayaimu: cabang yang salah di badan fungsi tidak akan tertangkap.
Distributive Conditional Type
Ketika tipe yang diperiksa adalah type parameter polos dan menerima union, kondisinya dijalankan sekali untuk setiap anggota dan hasilnya digabung menjadi union baru:
Distribusi inilah yang membuat Exclude dan Extract bekerja. Exclude<T, U> didefinisikan sebagai T extends U ? never : T: setiap anggota yang cocok dengan U menjadi never, dan never menghilang dari union. Jadi Exclude<"a" | "b" | "c", "a"> adalah "b" | "c".
Ada dua kejutan yang berasal dari aturan yang sama. boolean adalah union true | false, jadi ToArray<boolean> adalah false[] | true[], bukan boolean[]. Dan never adalah union kosong, jadi distributive conditional type yang menerima never mengembalikan never tanpa menguji apa pun:
type IsNever<T> = T extends never ? true : false;
type X = IsNever<never>; // never, not true
type IsNeverFixed<T> = [T] extends [never] ? true : false;
type Y = IsNeverFixed<never>; // true
Mengekstrak Tipe dengan infer
infer mendeklarasikan variabel tipe baru di dalam klausa extends. Kalau pencocokannya berhasil, TypeScript mengisi variabel itu dari tipe yang diperiksa, dan kamu bisa memakainya di cabang true:
Baca T extends Promise<infer V> ? V : T sebagai "kalau T adalah promise dari sesuatu, sebut sesuatu itu V lalu kembalikan; kalau tidak, kembalikan T apa adanya". infer hanya diizinkan di klausa extends sebuah conditional type.
Variabel infer bisa membawa constraint-nya sendiri dengan extends. Pencocokannya lalu hanya berhasil kalau tipe hasil inferensi memenuhinya:
type FirstString<T> = T extends [infer S extends string, ...unknown[]] ? S : never;
type A = FirstString<["a", 1]>; // "a"
type B = FirstString<[1, "a"]>; // never: the first element is not a string
Membuat ReturnType Sendiri
ReturnType bawaan adalah conditional type satu baris dengan infer. Menulisnya sendiri adalah latihan klasik yang membuat kedua konsep ini langsung dipahami:
typeof makeUser mengubah nilai fungsi menjadi tipenya, lalu conditional type mencocokkannya dengan "fungsi apa pun" dan menangkap tipe kembaliannya sebagai R. Versi di library standar berbeda dalam dua detail: parameternya dibatasi pada function type (T extends (...args: any) => any), jadi ReturnType<string> adalah error kompilasi, bukan never, dan cabang false-nya adalah any. Halaman ReturnType juga membahas Parameters, InstanceType, dan Awaited.
Conditional Type Rekursif
Conditional type bisa merujuk ke dirinya sendiri, sehingga bisa membuka sarang sedalam apa pun:
type Flatten<T> = T extends readonly (infer U)[] ? Flatten<U> : T;
type A = Flatten<number[][][]>; // number
type B = Flatten<string>; // string
Awaited<T> bawaan bekerja dengan cara ini, membuka Promise<Promise<T>> sampai ke T. Dalam praktiknya, jaga agar rekursinya dangkal: rekursi yang sangat dalam atau tanpa batas membuat compiler menyerah dengan error TS2589: Type instantiation is excessively deep and possibly infinite.
Referensi Cepat
| Pola | Arti |
|---|---|
T extends U ? X : Y | X kalau T bisa diberikan ke U, selain itu Y |
T extends U ? never : T | membuang anggota yang cocok dengan U (inilah Exclude) |
T extends U ? T : never | menyimpan anggota yang cocok dengan U (inilah Extract) |
[T] extends [U] ? X : Y | tes yang sama, tanpa distribusi ke union |
T extends (infer E)[] ? E : T | tipe elemen sebuah array |
T extends Promise<infer V> ? V : T | tipe nilai sebuah promise |
T extends (...args: any[]) => infer R ? R : never | tipe kembalian sebuah fungsi |
T extends [infer H, ...infer Rest] ? ... | elemen pertama dan sisa sebuah tuple |
Pertanyaan yang sering diajukan
Apa itu conditional type di TypeScript?
Tipe berbentuk T extends U ? X : Y. Kalau T bisa diberikan ke U, hasilnya X, kalau tidak, Y. Ini adalah if/else untuk tipe, yang dievaluasi saat kompilasi; tidak ada sedikit pun yang tersisa di JavaScript hasil kompilasi.
Apa fungsi kata kunci infer di TypeScript?
infer mendeklarasikan variabel tipe di dalam klausa extends sebuah conditional type dan membiarkan TypeScript mengisinya dari tipe yang cocok. T extends Promise<infer V> ? V : T mengambil tipe nilai hasil dari sebuah tipe promise. Kata kunci ini hanya bisa dipakai di klausa extends sebuah conditional type.
Apa itu distributive conditional type?
Ketika tipe yang diperiksa adalah type parameter polos dan kamu mengirim union, kondisinya diterapkan ke setiap anggota secara terpisah lalu hasilnya digabung. ToArray<string | number> dengan type ToArray<T> = T extends unknown ? T[] : never menjadi string[] | number[]. Bungkus kedua sisi dengan kurung siku, [T] extends [unknown], untuk mematikannya.
Bagaimana cara mendapatkan tipe kembalian sebuah fungsi di TypeScript?
Pakai ReturnType<typeof fn> bawaan. Itu adalah conditional type dengan infer: T extends (...args: any) => infer R ? R : any. Untuk fungsi async, bungkus dengan Awaited<...> untuk mendapatkan nilai hasilnya, bukan promise-nya.
Kenapa IsNever<never> mengembalikan never, bukan true?
never adalah union kosong, dan distributive conditional type memetakan setiap anggota union. Tanpa anggota, tidak ada yang dipetakan dan hasilnya never. Tulis [T] extends [never] ? true : false untuk menguji never itu sendiri.