Tipe Promise

Promise<T>, apa yang benar-benar dikembalikan fungsi async, dan cara compiler mengetiknya.

Daftar isi

Operasi yang memerlukan waktu tunggu, seperti membaca sensor atau mengambil data dari jaringan, tidak mengembalikan nilainya saat dipanggil. Topik ini membahas tipe Promise<T> sebagai wadah nilai yang datang kemudian, apa yang benar-benar dikembalikan fungsi async, serta cara compiler mengetikkan keduanya. Setelah membacanya, tanda tangan fungsi asinkron dapat dibaca dan ditulis dengan tepat.

Promise sebagai tipe nilai yang datang kemudian#

Promise adalah objek bawaan JavaScript yang merepresentasikan hasil operasi asinkron, yaitu operasi yang nilainya tersedia kemudian, bukan saat pemanggilannya. Sepanjang siklus hidupnya, janji berada dalam keadaan pending selama nilainya belum ada, lalu berpindah ke keadaan fulfilled apabila nilai akhirnya terisi. TypeScript mengetikkan objek tersebut sebagai Promise<T>, dengan T menyatakan tipe nilai yang diterima apabila operasinya berhasil. Sebuah Promise<number> berarti angka akan datang kemudian, sedangkan Promise<string> berarti teks. Anotasi ini mengubah dugaan bentuk nilai menjadi kontrak yang diperiksa compiler.

Berkas berikut membandingkan dua cara menghasilkan nilai asinkron pada pengukuran suhu ruangan. Fungsi biasa mengembalikan janji yang dibuat dengan Promise.resolve, sedangkan fungsi async menuliskan return nilai biasa. Keduanya dianotasi Promise<number>, dan hasil pencetakan memperlihatkan perbedaan antara janji dan nilainya.

ts sensor-suhu.ts
function bacaSuhu(): Promise<number> {
  return Promise.resolve(27);
}

async function ambilSuhu(): Promise<number> {
  return 27;
}

console.log(bacaSuhu());
console.log(ambilSuhu());
console.log(await bacaSuhu());
text Keluaran
Promise { 27 }
Promise { 27 }
27

Dua baris pertama memperlihatkan bahwa pemanggilan bacaSuhu() maupun ambilSuhu() menghasilkan objek Promise, bukan angkanya. Konsol Node menampilkannya sebagai Promise { 27 }, yaitu representasi janji yang sudah terisi nilai. Baris ketiga baru memperlihatkan angka 27 karena nilai itu diambil dengan await, yakni kata kunci untuk menunggu janji. Pola ini berlaku secara umum, yaitu nilai T hanya diperoleh setelah janjinya ditunggu.

Apa yang benar-benar dikembalikan fungsi async#

Fungsi yang diberi kata kunci async selalu mengembalikan objek Promise, sekalipun tubuhnya memuat return nilai biasa. Compiler membungkus nilai tersebut ke dalam Promise secara otomatis, sehingga anotasi kembaliannya wajib berbentuk Promise<T>. Fungsi async tanpa return menghasilkan Promise<void>, dengan void menyatakan tidak ada nilai yang dibawa. Bentuk ini lazim pada proses yang hasilnya tidak dipakai, misalnya pengiriman catatan ke berkas log. Karena pembungkusannya otomatis, penulisan return di dalam fungsi async tidak berarti kembaliannya adalah nilai mentah tersebut.

Aturan yang sama berlaku ketika tubuh fungsi mengembalikan janji yang sudah jadi, misalnya hasil pemanggilan fungsi lain. Janji di dalam janji tersebut diratakan menjadi satu, sehingga penulisan Promise<Promise<number>> tidak pernah diperlukan. Proses perataan ini menjaga tipe kembalian tetap rata dan mudah dibaca.

ts pembungkus-async.ts
async function kembalikanAngka(): Promise<number> {
  return 42;
}

async function kembalikanPromise(): Promise<number> {
  return Promise.resolve(42);
}

console.log(await kembalikanAngka());
console.log(await kembalikanPromise());
text Keluaran
42
42

Kedua bentuk penulisan menghasilkan angka yang sama setelah ditunggu, karena kembalian Promise.resolve(42) di dalam fungsi async menyatu dengan janji pembungkusnya. Perbedaannya terletak pada tempat pembungkusan dilakukan, bukan pada hasil akhirnya. Kembalian fungsi async tetap berupa janji, sehingga pemanggilannya harus ditunggu sebelum nilainya dipakai.

Cara compiler mengetikkan kode asinkron#

Compiler menerapkan dua aturan pengetikan pada kode asinkron. Pertama, anotasi kembalian fungsi async hanya menerima bentuk Promise<T>, sehingga anotasi tipe nilai polosnya ditolak. Kedua, ekspresi await di depan sebuah Promise<T> menghasilkan T, sehingga nilai yang sudah ditunggu dapat dipakai seperti nilai biasa.

Berkas berikut memeriksa kedua aturan itu pada pembacaan sensor yang sama. Variabel janji disimpulkan bertipe Promise<number>, lalu await menghasilkan suhu bertipe number, sehingga penjumlahannya sah. Anotasi eksplisit pada naik berperan sebagai pernyataan tipe, yaitu pemeriksaan yang gagal kompilasi apabila suhu bukan angka.

ts ketik-await.ts
async function ambilSuhu(): Promise<number> {
  return 27;
}

async function utama(): Promise<void> {
  const janji = ambilSuhu();
  const suhu = await janji;

  const naik: number = suhu + 3;

  console.log(suhu, naik);
}

utama();
text Keluaran
27 30
bash
# pernyataan tipe pada ketik-await.ts gagal kompilasi bila suhu bukan number
tsc --noEmit --pretty false ketik-await.ts && echo "tipe yang dinyatakan sesuai"
text Keluaran
tipe yang dinyatakan sesuai

Perintah tsc meloloskan berkas tersebut tanpa error, sehingga pernyataan const naik: number terbukti sesuai dengan tipe sebenarnya. Pernyataan tipe semacam ini mudah ditulis dan memberi bukti nyata bahwa await benar-benar menghasilkan number. Kebalikannya juga berlaku, yaitu janji yang belum ditunggu tidak boleh diperlakukan sebagai nilainya. Dua sifat itulah yang membedakan kode asinkron TypeScript dari JavaScript biasa, karena pemeriksaannya terjadi sebelum program berjalan.

Kesalahan umum#

Kesalahan pertama adalah menganotasi kembalian fungsi async dengan tipe nilai polos. Bentuk semacam ini lazim muncul saat fungsi biasa dikonversi menjadi fungsi async dengan menambahkan kata kunci async saja. Compiler menolaknya dengan pesan TS1064 yang sekaligus menyarankan perbaikannya, yaitu membungkus tipe tersebut dalam Promise.

ts async-salah.ts
// tidak-dijalankan
async function ambilNilai(): number {
  return 42;
}
bash
# membuktikan pesan error untuk anotasi kembalian yang tidak dibungkus Promise
tsc --noEmit --pretty false async-salah.ts
text Keluaran
async-salah.ts(2,30): error TS1064: The return type of an async function or method must be the global Promise<T> type. Did you mean to write 'Promise<number>'?

Kesalahan kedua adalah melupakan await. Variabel yang seharusnya menampung angka justru menampung janjinya, dan penugasan semacam itu ditolak dengan TS2322 karena Promise<number> tidak dapat dianggap sebagai number. Perbaikannya berupa penambahan await di depan pemanggilan, sehingga variabelnya menampung nilai yang sudah tersedia.

ts tanpa-await.ts
// tidak-dijalankan
async function ambilSuhu(): Promise<number> {
  return 27;
}

const suhu: number = ambilSuhu();
bash
# membuktikan pesan error untuk janji yang ditugaskan ke variabel number
tsc --noEmit --pretty false tanpa-await.ts
text Keluaran
tanpa-await.ts(6,7): error TS2322: Type 'Promise<number>' is not assignable to type 'number'.

Kesalahan ketiga muncul ketika berkas yang memakai await di tingkat atas diperiksa dengan tsc. Pesan TS1375 menyatakan bahwa await tingkat atas hanya sah pada berkas modul, yaitu berkas yang memuat import atau export. Perbaikannya berupa penambahan export {} kosong atau pembungkusan kode dalam fungsi. Eksekusi langsung lewat Node.js tidak memerlukan keduanya karena berkas dengan pernyataan impor terdeteksi sebagai modul secara otomatis.

Ringkasan#

  • Promise<T> menjanjikan nilai bertipe T yang datang kemudian, dan nilai itu diperoleh setelah janjinya ditunggu dengan await.
  • Fungsi async selalu mengembalikan Promise<T>, sekalipun tubuhnya memuat return nilai biasa, dan janji bertingkat diratakan secara otomatis.
  • Anotasi kembalian fungsi async wajib berbentuk Promise<T>, sedangkan await menghasilkan T sehingga nilainya dapat dipakai seperti nilai biasa.
  • Pemanggilan tanpa await menghasilkan objek Promise, bukan nilai, sehingga operasi yang mengharapkan nilai biasa ditolak compiler.

Referensi#