Fungsi dan tipe yang ditulis ulang untuk setiap tipe datanya cepat menjadi duplikasi. Generics mengatasinya melalui parameter tipe, yaitu penampung tipe yang diisi pada saat pemanggilan. Topik ini membahas penulisan parameter tipe pada fungsi dan tipe, inferensi di titik pemanggilan, serta peran Array<T> sebagai generics bawaan yang dipakai sehari-hari.
Parameter tipe#
Parameter tipe ditulis di dalam tanda kurung siku setelah nama fungsi, dan setiap anotasi di dalamnya boleh merujuk ke parameter tersebut. Konvensi penamaannya berupa huruf kapital tunggal seperti T, K, dan V. Satu parameter tipe menghubungkan beberapa titik dalam tanda tangan, sehingga perubahan pada satu titik otomatis diikuti oleh titik lainnya.
function pertama<T>(daftar: T[]): T | undefined {
return daftar.length > 0 ? daftar[0] : undefined;
}
const skorPertama = pertama([80, 90, 100]);
const namaPertama = pertama(["Ali", "Budi", "Citra"]);
console.log(skorPertama);
console.log(namaPertama);80
AliTanpa generics, fungsi pertama harus ditulis dua kali, satu untuk array number dan satu untuk array string, atau ditulis dengan any yang menghilangkan seluruh pemeriksaan. Parameter tipe T menghubungkan tipe argumen dengan tipe kembalian, sehingga keduanya berubah bersama. Hasilnya, pemanggilan dengan array number menghasilkan number | undefined, sedangkan pemanggilan dengan array string menghasilkan string | undefined.
type Kotak<T> = {
isi: T;
};
const kotakNilai: Kotak<number> = { isi: 95 };
const kotakNama: Kotak<string> = { isi: "rapor" };
console.log(kotakNilai.isi, kotakNama.isi);95 raporParameter tipe juga berlaku pada type alias dan interface. Tipe Kotak pada contoh adalah cetakan, dan Kotak<number> mengisi T dengan number, sedangkan Kotak<string> mengisi T dengan string. Satu definisi menghasilkan banyak tipe tanpa menduplikasi bentuknya.
Sebuah fungsi boleh memiliki lebih dari satu parameter tipe, sebagaimana pasangkan<K, V> pada bagian berikutnya, dan setiap parameter diisi secara terpisah. Parameter yang sama juga boleh muncul beberapa kali dalam satu tanda tangan, dan setiap kemunculannya wajib cocok, sehingga dua argumen terikat tipe yang sama. Ikatan semacam itulah yang membuat generics mampu menyatakan hubungan antarargumen, bukan sekadar tipe yang longgar.
Inferensi di titik pemanggilan#
Compiler TypeScript menyimpulkan isi parameter tipe dari argumen pemanggilan, sehingga kurung siku jarang perlu ditulis secara eksplisit. Pemanggilan pasangkan("piket", 3) tidak menyebut tipe apa pun, tetapi hasilnya tetap ketat. Berkas berikut membuktikan hasil inferensi tersebut dengan pernyataan tipe yang gagal kompilasi bila keliru.
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;
function pasangkan<K, V>(kunci: K, nilai: V): [K, V] {
return [kunci, nilai];
}
const entri = pasangkan("piket", 3);
const cekTipe: Expect<Equal<typeof entri, [string, number]>> = true;
console.log(entri);[ 'piket', 3 ]# pernyataan tipe pada cek-inferensi.ts gagal kompilasi bila inferensinya keliru
tsc --noEmit cek-inferensi.ts && echo "pernyataan tipe terbukti benar"pernyataan tipe terbukti benarTipe Expect hanya menerima true, sedangkan Equal membandingkan dua tipe dengan ketat, sehingga cekTipe gagal kompilasi apabila tipe entri bukan [string, number]. Pemeriksaan tsc yang berakhir tanpa error menegaskan bahwa inferensi menghasilkan tuple [string, number] dari argumen "piket" dan 3. Penulisan eksplisit seperti pasangkan<string, number>("piket", 3) tetap sah, tetapi hanya diperlukan ketika inferensi tidak memiliki cukup petunjuk.
Inferensi bekerja dari argumen menuju parameter tipe, bukan sebaliknya. Parameter tipe yang tidak pernah muncul pada daftar parameter tidak memiliki petunjuk sama sekali, sehingga compiler memilih unknown dan anotasi eksplisit menjadi satu-satunya cara mengarahkannya. Apabila sebagian parameter tipe ditulis secara eksplisit, sisanya tetap disimpulkan dari argumen sesuai posisinya.
Menyingkap Array#
Array yang selama ini ditulis number[] sebenarnya adalah singkatan dari Array<number>, yaitu generics bawaan bernama Array dengan satu parameter tipe T. Seluruh metode array menurunkan tipenya dari T, sehingga map pada Array<number> menetapkan parameternya bertipe number dan push hanya menerima number. Perilaku itu bukan keistimewaan khusus array, melainkan generics yang sama seperti yang ditulis sendiri pada topik ini.
const nilaiUjian: Array<number> = [80, 90, 100];
const namaSiswa: Array<string> = ["Ali", "Budi"];
const naikSepuluh = nilaiUjian.map((nilai) => nilai + 10);
const kapital = namaSiswa.map((nama) => nama.toUpperCase());
console.log(naikSepuluh);
console.log(kapital);[ 90, 100, 110 ]
[ 'ALI', 'BUDI' ]Callback (nilai) => nilai + 10 pada contoh tidak diberi anotasi apa pun, tetapi nilai diketahui bertipe number karena tanda tangan map pada Array<number> mendeklarasikannya. Pola yang sama membuat nama.toUpperCase() sah pada array string. Memahami Array<T> sebagai generics sekaligus menjelaskan mengapa tipe kembalian metode mengikuti isi array.
Tanda tangan map memuat parameter tipe kedua untuk hasilnya, sehingga callback yang mengembalikan tipe lain otomatis menghasilkan array bertipe baru. Pemanggilan nilaiUjian.map((nilai) => nilai > 85) menghasilkan array boolean tanpa satu pun anotasi. Perilaku ini memperlihatkan generics bekerja di dua arah, yaitu menurunkan tipe masukan dan menyimpulkan tipe keluaran.
Kesalahan umum#
Kekeliruan pertama adalah mengakses properti pada T tanpa batasan. Parameter tipe polos tidak menjanjikan properti apa pun, sehingga penulisan nilai.nama di dalam fungsi generik menghasilkan error TS2339 yang menyatakan properti tidak ada pada T. Perbaikannya adalah membatasi T dengan extends agar bentuk minimumnya diketahui compiler.
Kekeliruan kedua adalah menulis anotasi tipe eksplisit yang sebenarnya dapat disimpulkan. Penulisan pertama<number>([80, 90, 100]) menambah kebisingan tanpa menambah jaminan, karena inferensi sudah menghasilkan tipe yang sama. Anotasi eksplisit tetap berguna ketika hasil yang diinginkan berbeda dari argumennya, misalnya memaksa tipe kembalian yang lebih luas, sehingga pilihan ini ditentukan oleh kebutuhan, bukan kebiasaan.
Kekeliruan ketiga adalah mengira generics menambah biaya pada saat runtime. Seluruh parameter tipe dihapus saat kompilasi, sehingga JavaScript yang dihasilkan identik dengan kode yang ditulis tanpa generics. Kegunaannya sepenuhnya berada pada pemeriksaan tipe, dokumentasi tanda tangan, dan dukungan editor.
Ringkasan#
- Parameter tipe menghubungkan tipe masukan, tipe kembalian, dan tipe internal sebuah fungsi atau alias dalam satu kontrak.
- Compiler mengisi parameter tipe lewat inferensi di titik pemanggilan, dan pernyataan tipe seperti
Expectdapat membuktikan hasilnya. Array<T>adalah generics bawaan, denganT[]sebagai singkatannya, sehingga seluruh metode array ikut menurunkan tipenya dariT.anymenghilangkan semua pemeriksaan, sedangkan generics mempertahankannya tanpa menduplikasi kode.
Referensi#
- Generics — The TypeScript Handbook — Dokumentasi resmi parameter tipe, generic types, dan generic constraints.