Fungsi asinkron yang ditulis ulang untuk setiap bentuk data cepat menjadi duplikasi. Generics, yaitu penulisan satu fungsi untuk banyak tipe data, menyelesaikan duplikasi tersebut. Topik ini membahas pengetikan fungsi bantu (helper) asinkron yang generik, yaitu helper retry dengan jumlah percobaan tetap, pemetaan atas sekumpulan Promise, serta Promise.all yang menyatukan banyak janji sekaligus. Setelah membacanya, helper asinkron dapat ditulis satu kali untuk semua tipe datanya.
Helper retry yang generik#
Permintaan ke sumber daya luar kadang gagal karena gangguan yang sementara, misalnya sensor yang belum siap saat aplikasi dimulai. Helper retry menangani kasus itu dengan menjalankan tugas yang sama hingga berhasil atau batas percobaan tercapai. Secara generik, helper ini ditulis cobaUlang<T>(tugas: () => Promise<T>, batas: number): Promise<T>, dan parameter tipe T menghubungkan tugas yang dibungkus dengan kembalian helper tersebut.
async function cobaUlang<T>(tugas: () => Promise<T>, batas: number): Promise<T> {
let terakhir: unknown;
for (let percobaan = 1; percobaan <= batas; percobaan++) {
try {
return await tugas();
} catch (e) {
terakhir = e;
}
}
throw terakhir;
}
let panggilan = 0;
async function bacaSensor(): Promise<number> {
panggilan++;
if (panggilan < 3) {
throw new Error("sensor belum siap");
}
return 27;
}
const suhu = await cobaUlang(bacaSensor, 5);
console.log(suhu);
console.log(panggilan);27
3Sensor pada contoh gagal dua kali dengan error “sensor belum siap”, lalu berhasil pada percobaan ketiga. Nilai panggilan yang tercetak adalah 3, dan angka itu membuktikan helper benar-benar menjalankan ulang tugasnya, sedangkan hasil akhirnya tetap number. Karena argumen tugas bertipe () => Promise<number>, compiler menyimpulkan T sebagai number, sehingga suhu langsung bertipe number tanpa anotasi tambahan. Variabel terakhir sengaja dianotasi unknown karena nilai lemparan dari tugas tidak dijanjikan bertipe tertentu.
Parameter tugas berbentuk fungsi, bukan nilai Promise<T> yang sudah jadi, dan pilihan ini bukan gaya semata. Sebuah janji sudah berjalan sejak dibuat, sehingga mengirimkannya sebagai argumen membuat percobaan ulang tidak berarti. Bentuk () => Promise<T> menunda pembuatan janji hingga helper memanggilnya, sehingga setiap percobaan benar-benar memulai tugas baru.
Pemetaan atas Promise#
Fungsi map pada array tetap berjalan serentak terhadap fungsi async, tetapi hasilnya bukan array nilai, melainkan array janji. Untuk konversi suhu Celcius ke Fahrenheit, suhuMentah.map(keFahrenheit) menghasilkan Promise<number>[], yaitu tiga janji yang masing-masing membawa satu angka. Tipe Promise<number>[] berbeda dengan Promise<number[]>, dan perbedaan inilah yang paling sering keliru dibaca.
const suhuMentah = [21, 24, 27];
async function keFahrenheit(celcius: number): Promise<number> {
return (celcius * 9) / 5 + 32;
}
const janjiSuhu = suhuMentah.map(keFahrenheit);
console.log(janjiSuhu.length);
const fahrenheit = await Promise.all(janjiSuhu);
console.log(fahrenheit);3
[ 69.8, 75.2, 80.6 ]Baris pertama keluaran menunjukkan bahwa array janji sudah ada sebelum satu pun nilainya diambil. Pemanggilan Promise.all(janjiSuhu) menerima array Promise<number>[] dan mengembalikan Promise<number[]>, sehingga setelah ditunggu, fahrenheit bertipe number[], yaitu array angka biasa. Nilainya mudah dihitung ulang, yaitu 21 dikali 9 dibagi 5 ditambah 32 menghasilkan 69.8, dan pola yang sama memberikan 75.2 serta 80.6.
Helper petaAsync yang generik#
Pola tunggu-lalu-kumpulkan tersebut dapat dibungkus menjadi helper tersendiri agar tidak diulang di setiap pemanggilan. Tipe yang tepat adalah petaAsync<T, U>(nilai: T[], tugas: (x: T) => Promise<U>): Promise<U[]>, dengan dua parameter tipe yang masing-masing melayani satu sisi. Parameter T menyatakan isi array masukan, sedangkan U menyatakan hasil konversi yang dibawa setiap janjinya.
async function petaAsync<T, U>(nilai: T[], tugas: (x: T) => Promise<U>): Promise<U[]> {
return Promise.all(nilai.map(tugas));
}
const suhuMentah = [21, 24, 27];
const fahrenheit = await petaAsync(suhuMentah, async (celcius) => (celcius * 9) / 5 + 32);
console.log(fahrenheit);[ 69.8, 75.2, 80.6 ]Tidak ada satu pun anotasi tipe di titik pemanggilan, karena compiler menyimpulkan T sebagai number dari isi suhuMentah dan U sebagai number dari kembalian callback-nya. Apabila callback mengembalikan teks, U otomatis menjadi string dan hasilnya string[]. Satu definisi helper dengan demikian melayani semua kombinasi tipe, tanpa any dan tanpa menduplikasi tanda tangan.
Promise.all dan tipe tuple#
Ketika janji-janji yang digabungkan bertipe campur, hasil Promise.all tetap berupa tuple dengan posisi yang terjaga. Pemanggilan Promise.all([bacaLabel(), bacaSuhu()]) menghasilkan [string, number], bukan (string | number)[], karena overload-nya, yaitu tanda tangan khusus pada definisi Promise.all, menangani array literal. Berkas berikut membuktikan hal itu dengan pernyataan tipe yang gagal kompilasi bila tipe gabung berbeda dari yang dinyatakan.
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;
async function bacaLabel(): Promise<string> {
return "ruang server";
}
async function bacaSuhu(): Promise<number> {
return 27;
}
async function utama(): Promise<void> {
const gabung = await Promise.all([bacaLabel(), bacaSuhu()]);
const cekTipe: Expect<Equal<typeof gabung, [string, number]>> = true;
console.log(gabung);
}
utama();[ 'ruang server', 27 ]# pernyataan tipe pada gabung-janji.ts gagal kompilasi bila hasilnya bukan [string, number]
tsc --noEmit --pretty false gabung-janji.ts && echo "pernyataan tipe terbukti benar"pernyataan tipe terbukti benarPernyataan cekTipe hanya lolos apabila gabung bertipe [string, number], dan pemeriksaan tsc yang keluar tanpa error menegaskannya. Tuple seperti ini berguna karena posisi tetap membawa makna, yaitu elemen pertama pasti teks dan elemen kedua pasti angka. Akses gabung[0] dengan demikian bertipe string tanpa pempersempit tambahan. Apabila seluruh hasil dibutuhkan meskipun sebagian tugas gagal, Promise.allSettled menyediakan kembalian lain yang berupa laporan status per janji.
Kesalahan umum#
Kesalahan pertama adalah memperlakukan array janji seolah-olah array nilai. Callback async di dalam map menghasilkan Promise<number>[], dan pemanggilan metode angka atas janjinya ditolak compiler dengan TS2339. Perbaikannya berupa penambahan Promise.all sebelum hasilnya dipakai.
// tidak-dijalankan
const suhuMentah = [21, 24, 27];
const janjiSuhu = suhuMentah.map(async (celcius) => (celcius * 9) / 5 + 32);
const naik = janjiSuhu.map((janji) => janji.toFixed(1));# membuktikan pesan error untuk array janji yang dipakai sebagai array nilai
tsc --noEmit --pretty false tanpa-all.tstanpa-all.ts(6,45): error TS2339: Property 'toFixed' does not exist on type 'Promise<number>'.Kesalahan kedua adalah mengetik helper retry tanpa parameter tipe, misalnya dengan tugas: () => Promise<any>. Helper semacam itu tetap berjalan, tetapi hasilnya bertipe any, sehingga salah ketik pada pemakaiannya tidak tertangkap sampai program berjalan. Parameter tipe T tidak menambah biaya apa pun, tetapi memindahkan pemeriksaan tersebut ke waktu kompilasi.
Kesalahan ketiga adalah memakai helper retry tanpa batas percobaan. Tugas yang gagal permanen akan diulang tanpa henti, sehingga program menggantung dan sumber dayanya terkuras. Batas percobaan pada contoh di atas membatasi jumlah pemanggilan, dan pada kode nyata batas itu biasanya disertai jeda singkat antarpercobaan.
Ringkasan#
- Parameter tipe menghubungkan tugas yang dibungkus dengan kembalian helper, sehingga
cobaUlang<T>menghasilkanPromise<T>yang mengikuti tugasnya. mapatas fungsi async menghasilkan array janjiPromise<U>[], sedangkanPromise.allmengubahnya menjadiPromise<U[]>.- Helper generik seperti
petaAsync<T, U>ditulis satu kali, sedangkan tipenya tersimpulkan otomatis dari argumen di titik pemanggilan. Promise.allatas array literal bertipe campur menghasilkan tuple yang posisinya tetap, misalnya[string, number].
Referensi#
- Promise.all — MDN Web Docs — Perilaku Promise.all beserta bentuk kembaliannya untuk array dan tuple.
- Generics — The TypeScript Handbook — Dokumentasi resmi parameter tipe dan inferensinya.
- Array.prototype.map() — MDN Web Docs — Cara kerja map yang menjadi dasar pemetaan atas Promise.