Infer

Mengekstrak tipe di dalam conditional types dengan kata kunci infer.

Daftar isi

Conditional type tidak berhenti pada memilih salah satu dari dua hasil, karena tipe ini juga dapat menangkap sepotong tipe dari bentuk yang dicocokkannya. Tugas itu dipegang oleh kata kunci infer, yang mendeklarasikan variabel tipe di dalam klausa extends dan mengisinya dengan tipe yang cocok dengan pola tersebut. Topik ini membahas pola-pola klasik penangkapan tipe, penurunan tipe kembalian fungsi, pembacaan hasil resolusi lewat pesan compiler, serta perilaku variabel infer yang dipakai ulang.

Menangkap tipe dari pola bentuk#

Kata kunci infer bekerja seperti pencocokan pola (pattern matching) pada posisi tipe, yaitu compiler mencocokkan T terhadap sebuah pola bentuk dan mengisi setiap variabel infer yang ditemukan di dalamnya. Pola (infer U)[] menyatakan array dengan tipe elemen yang belum diketahui, sehingga U otomatis terisi tipe elemennya ketika T cocok. Pola yang sama berlaku pada Promise<infer U>, yang menangkap tipe nilai di dalam sebuah Promise. Penangkapan ini terjadi seluruhnya saat kompilasi dan tidak menyisakan apa pun saat program berjalan.

Berkas berikut merangkum dua pola klasik tersebut beserta bukti kesetaraannya. Tipe ElemenDari mengembalikan never ketika T bukan array, karena tidak ada cabang cadangan yang didefinisikan. Sebaliknya, BukaJanji menyediakan cabang cadangan berupa T itu sendiri, sehingga tipe non-Promise diteruskan tanpa diubah.

ts ekstrak-elemen.ts
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 ElemenDari<T> = T extends (infer U)[] ? U : never;
type BukaJanji<T> = T extends Promise<infer U> ? U : T;

const b1: Expect<Equal<ElemenDari<string[]>, string>> = true;
const b2: Expect<Equal<ElemenDari<number[][]>, number[]>> = true;
const b3: Expect<Equal<BukaJanji<Promise<string>>, string>> = true;
const b4: Expect<Equal<BukaJanji<number>, number>> = true;

const nama: ElemenDari<string[]> = "arsip-2024";
const isi: BukaJanji<Promise<number>> = 42;

console.log(b1, b2, b3, b4, nama, isi);
text Keluaran
true true true true arsip-2024 42

Dua baris terakhir menunjukkan pemakaian hasilnya sebagai tipe biasa. Variabel nama dianotasi dengan ElemenDari<string[]> yang resolusinya string, sedangkan isi dianotasi dengan BukaJanji<Promise<number>> yang resolusinya number. Pola-pola ini menjadi fondasi banyak utility type, yakni alat bantu tipe bawaan TypeScript, sehingga mengenali bentuk pola-pola ini memudahkan pembacaan alat bantu lain yang dibangun di atasnya.

Pola infer paling sering dipakai untuk membaca tipe kembalian sebuah fungsi tanpa mengulang anotasinya. Pola (...args: never[]) => infer R mencocokkan tipe fungsi dengan daftar parameter apa pun, karena never[] dapat diteruskan ke daftar parameter bertipe apa pun, lalu menangkap tipe kembaliannya pada R. Bentuk ini adalah inti dari utility type bawaan ReturnType, dan menulisnya sendiri membuat perilakunya mudah dipahami. Latihan kecil semacam ini juga melatih pembacaan pola yang jauh lebih rumit pada pustaka lain.

Berkas berikut mendefinisikan KembalianDari, membuktikan hasilnya setara dengan number, lalu memakai tipe hasil itu sebagai anotasi. Nilai hasil dihitung saat program berjalan, sehingga baris keluaran membuktikan tipe yang tertangkap di R benar-benar dapat dipakai untuk menganotasi nilai nyata.

ts kembalian-fungsi.ts
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 KembalianDari<T> = T extends (...args: never[]) => infer R ? R : never;

function suhuRataRata(nilai: number[]): number {
  const total = nilai.reduce((a: number, b) => a + b, 0);
  return total / nilai.length;
}

type TipeHasil = KembalianDari<typeof suhuRataRata>;

const b1: Expect<Equal<TipeHasil, number>> = true;

const hasil: TipeHasil = suhuRataRata([21, 24, 27]);

console.log(b1, hasil);
text Keluaran
true 24

Fungsi suhuRataRata menerima array angka dan mengembalikan rata-ratanya. Dari anotasi kembalian pada definisinya, compiler mengetahui tipe hasilnya adalah number. Pemanggilan KembalianDari<typeof suhuRataRata> kemudian menangkap number itu tanpa menyebutnya ulang, sehingga perubahan anotasi kembalian otomatis mengalir ke semua tipe yang diturunkan dari fungsi tersebut. Paduan typeof dan infer semacam ini menghilangkan duplikasi antara tanda tangan fungsi dan tipe turunannya.

Membaca hasil resolusi lewat pesan compiler#

Tipe tidak memiliki bentuk saat program berjalan, sehingga hasil resolusi conditional type tidak dapat dicetak dengan console.log. Cara paling langsung untuk melihat hasilnya adalah memicu error yang menyebut tipe hasil resolusi, yaitu dengan menganotasi sebuah properti memakai tipe yang diuji lalu mengisinya dengan nilai yang salah. Pesan error compiler kemudian menampilkan bentuk tipe yang diharapkan, dan bentuk itulah hasil resolusinya.

ts resolusi-infer.ts
// tidak-dijalankan
type BukaJanji<T> = T extends Promise<infer U> ? U : T;

const cek: { res: BukaJanji<Promise<number>> } = { res: "bukan ini" };

console.log(cek);
bash
# berkas ini sengaja tidak lolos pemeriksaan tipe
tsc --noEmit --pretty false resolusi-infer.ts
text Keluaran
resolusi-infer.ts(4,52): error TS2322: Type 'string' is not assignable to type 'number'.

Pesan menyebut number untuk properti res, padahal yang ditulis pada anotasinya adalah BukaJanji<Promise<number>>. Ketidakcocokan itu membuktikan bahwa conditional type sudah selesai dihitung sebelum compiler memeriksa penugasan nilai, dan hasilnya persis number. Teknik ini bekerja untuk perhitungan level tipe apa pun, termasuk mapped type dan tipe rekursif, sehingga menjadi kaca pembesar utama saat mengembangkan tipe sendiri.

Aturan penempatannya ketat, yaitu infer hanya boleh berada di dalam klausa extends sebuah conditional type. Menaruhnya di cabang hasil menghasilkan error sintaks yang menjelaskan batas tersebut. Berkas berikut memuat pelanggaran satu baris untuk melihat pesannya.

ts posisi-infer.ts
// tidak-dijalankan
type SalahPosisi<T> = T extends string ? infer U : never;
bash
# kata kunci infer sengaja ditempatkan di cabang hasil
tsc --noEmit --pretty false posisi-infer.ts
text Keluaran
posisi-infer.ts(2,42): error TS1338: 'infer' declarations are only permitted in the 'extends' clause of a conditional type.

Pesan TS1338 menyebut klausa extends secara eksplisit, sehingga perbaikannya jelas, yaitu memindahkan deklarasi variabel ke dalam pola yang dicocokkan. Aturan ini menjaga struktur conditional type tetap dapat dibaca, karena semua variabel yang ditangkap dideklarasikan pada polanya. Pembaca kode cukup melihat satu tempat untuk mengetahui tipe apa saja yang diekstrak, tanpa perlu menyisir cabang-cabang hasilnya.

Variabel infer yang dipakai ulang#

Satu variabel infer boleh muncul lebih dari sekali pada pola yang sama, dan setiap kemunculannya mengumpulkan kandidat tipe. Cara kandidat digabungkan mengikuti arah posisinya, yaitu kandidat dari posisi kovarian seperti properti objek digabungkan dengan union, sedangkan kandidat dari posisi kontravarian seperti parameter fungsi digabungkan dengan irisan tipe. Perilaku ini membuat variabel yang dipakai ulang berperan sebagai syarat kesesuaian antar posisi. Berkas berikut menguji kedua arah tersebut dengan dua tipe yang polanya mirip.

ts infer-berulang.ts
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 DuaProperti<T> = T extends { a: infer U; b: infer U } ? U : never;
type DuaFungsi<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void } ? U : never;

const b1: Expect<Equal<DuaProperti<{ a: string; b: string }>, string>> = true;
const b2: Expect<Equal<DuaProperti<{ a: string; b: number }>, string | number>> = true;
const b3: Expect<Equal<DuaFungsi<{ a: (x: string) => void; b: (x: string) => void }>, string>> = true;
const b4: Expect<Equal<DuaFungsi<{ a: (x: string) => void; b: (x: number) => void }>, string & number>> = true;

console.log(b1, b2, b3, b4);
text Keluaran
true true true true

Tipe DuaProperti menangkap U dari dua properti sekaligus, sehingga { a: string; b: number } menghasilkan string | number. Tipe DuaFungsi menangkap U dari parameter dua fungsi, dan hasilnya string & number, yaitu irisan tipe yang tidak memiliki anggota sehingga perilakunya sama dengan never. Ketika kedua posisi menuntut tipe yang sama persis, hasilnya tetap tipe itu, sedangkan ketika keduanya berbeda, union atau irisan tipe yang dihasilkan menyimpan seluruh informasi kandidatnya.

Kesalahan umum#

Kekeliruan pertama adalah memakai KembalianDari pada tipe fungsi yang masih generik. Deklarasi function identitas<T>(x: T): T bertipe <T>(x: T) => T, dan penangkapan R atas bentuk itu menghasilkan unknown, bukan tipe konkret pemanggilannya. Perbaikannya adalah menurunkan tipe dari fungsi yang sudah konkret, misalnya fungsi pembungkus dengan anotasi kembalian yang tertulis jelas. Hasil unknown tidak salah secara teknis, tetapi terlalu umum untuk dipakai sebagai anotasi yang bermakna.

Kekeliruan kedua adalah mengira cabang yang tidak cocok selalu menghasilkan never. Hasilnya ditentukan cabang cadangan yang ditulis, sehingga ElemenDari<number> bernilai never karena tidak punya cabang cadangan, sedangkan BukaJanji<number> bernilai number karena cadangannya T. Pilih cadangan sesuai maksudnya, yaitu never untuk menyaring tipe yang tidak relevan dan T untuk meneruskan apa adanya.

Kekeliruan ketiga adalah lupa bahwa masukan union diproses per anggota. Pemanggilan BukaJanji<Promise<string> | number> menghasilkan string | number, bukan satu tipe tunggal, karena parameter tipe polos pada posisi syarat terdistribusi atas union. Bila yang dimaksud adalah pemeriksaan keseluruhan, parameternya dibungkus tanda kurung siku terlebih dahulu.

Ringkasan#

  • Kata kunci infer mendeklarasikan variabel tipe di dalam klausa extends yang terisi otomatis dari pola bentuk yang dicocokkan.
  • Pola (infer U)[], Promise<infer U>, dan (...args: never[]) => infer R menangkap tipe elemen array, tipe isi Promise, dan tipe kembalian fungsi.
  • infer hanya sah di klausa extends, dan pelanggarannya dilaporkan compiler sebagai error TS1338.
  • Variabel infer yang dipakai ulang mengumpulkan kandidat, dengan union pada posisi kovarian dan irisan tipe pada posisi kontravarian.

Referensi#