Interop dengan JavaScript

allowJs, checkJs, dan pengetikan JSDoc untuk adopsi TypeScript secara bertahap.

Daftar isi

Sebagian besar kode JavaScript yang sudah ada tidak memiliki anotasi tipe, sehingga migrasi ke TypeScript jarang dilakukan dengan menulis ulang semuanya sekaligus. Topik ini membahas tiga mekanisme yang membuat kode JavaScript dan TypeScript dapat berdampingan, yaitu opsi allowJs untuk menyertakan berkas JavaScript, opsi checkJs untuk ikut memeriksanya, dan anotasi JSDoc untuk memberi tipe tanpa mengubah ekstensi berkas. Pembahasannya memakai dua proyek kecil yang berisi satu modul JavaScript dan satu pemanggil TypeScript. Setelah membacanya, migrasi dapat dimulai dari titik mana pun dengan pemeriksaan tipe yang tetap berjalan.

Mengizinkan berkas JavaScript dengan allowJs#

Secara bawaan, compiler hanya memeriksa berkas .ts dan menolak membaca isi berkas .js sebagai bagian dari program. Opsi allowJs mengubah aturan ini, sehingga berkas JavaScript ikut dikompilasi dan tipenya dapat disimpulkan dari isinya. Modul JavaScript yang diekspor melalui module.exports tetap dapat dipanggil dari TypeScript, dan esModuleInterop membuat pemanggilan tersebut dapat ditulis dengan sintaks import biasa.

Proyek proyek-allowjs di bawah ini berisi modul statistik.js tanpa satu pun anotasi tipe, beserta aplikasi.ts yang memanggilnya. Perhatikan pemanggilan kedua yang mengirim string ke fungsi yang seharusnya mengolah daftar angka.

json proyek-allowjs/tsconfig.json
{
  "compilerOptions": {
    "allowJs": true,
    "noEmit": true,
    "strict": true,
    "module": "commonjs",
    "target": "es2022",
    "esModuleInterop": true
  }
}
js proyek-allowjs/statistik.js
function rataRata(nilai) {
  let total = 0;
  for (const n of nilai) {
    total += n;
  }
  return total / nilai.length;
}

module.exports = { rataRata };
ts proyek-allowjs/aplikasi.ts
// tidak-dijalankan
import { rataRata } from "./statistik.js";

console.log(rataRata([8, 7, 9]));
console.log(rataRata("delapan"));
bash
# memeriksa proyek yang mengizinkan berkas JavaScript
tsc --project proyek-allowjs --pretty false

Perintah di atas berakhir tanpa pesan error apa pun, padahal argumen "delapan" jelas salah sasaran. Penyebabnya ada pada parameter nilai yang tidak dianotasi, sehingga dari sisi TypeScript fungsi ini menerima argumen apa pun. Perilaku saat program dijalankan pun mudah ditebak.

bash
# menjalankan aplikasi langsung dari berkas TypeScript-nya
( cd proyek-allowjs && node --no-warnings aplikasi.ts )
text Keluaran
8
NaN

Baris pertama benar karena argumennya berupa daftar angka. Baris kedua menghasilkan NaN karena string "delapan" diperlakukan sebagai kumpulan karakter, ditambahkan satu per satu ke penampung angka, lalu dibagi dengan panjangnya. Kesalahan semacam ini baru terlihat saat program berjalan, dan justru itulah yang ingin dihilangkan oleh langkah berikutnya.

Memeriksa JavaScript dengan checkJs#

Opsi checkJs memerintahkan compiler melaporkan error di dalam berkas JavaScript dengan cara yang sama seperti pada berkas TypeScript. Tanpa anotasi apa pun, laporan ini terbatas pada kesalahan sintaks dan perilaku yang pasti salah. Anotasi JSDoc, yaitu komentar berawalan /** yang mendeskripsikan tipe, mengubah keadaan itu. Tipe yang dinyatakan di dalamnya diperiksa dengan kekuatan penuh, termasuk pada setiap pemanggil dari berkas TypeScript.

Proyek proyek-jsdoc memakai konfigurasi yang sama dengan proyek-allowjs, ditambah "checkJs": true. Modul statistik.js kini membawa anotasi @param dan @returns, sedangkan pemanggil yang salah tetap dipertahankan.

json proyek-jsdoc/tsconfig.json
{
  "compilerOptions": {
    "allowJs": true,
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "module": "commonjs",
    "target": "es2022",
    "esModuleInterop": true
  }
}
js proyek-jsdoc/statistik.js
/**
 * Menghitung rata-rata dari daftar nilai.
 * @param {number[]} nilai
 * @returns {number}
 */
function rataRata(nilai) {
  let total = 0;
  for (const n of nilai) {
    total += n;
  }
  return total / nilai.length;
}

module.exports = { rataRata };
ts proyek-jsdoc/aplikasi.ts
// tidak-dijalankan
import { rataRata } from "./statistik.js";

console.log(rataRata("delapan"));
bash
# memeriksa proyek dengan checkJs dan anotasi JSDoc
tsc --project proyek-jsdoc --pretty false
text Keluaran
proyek-jsdoc/aplikasi.ts(4,22): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number[]'.

Kesalahan yang sebelumnya lolos kini tertangkap, dan lokasinya justru pada berkas pemanggil. Anotasi @param {number[]} pada fungsi rataRata menjadi kontrak yang berlaku dua arah, yaitu isi berkas JavaScript ikut diperiksa terhadapnya dan setiap pemanggil TypeScript ditolak bila melanggarnya. Pola dua berkas dengan ekstensi berbeda ini menjadi dasar adopsi bertahap.

Anotasi JSDoc untuk adopsi bertahap#

JSDoc mencakup anotasi untuk hampir semua kebutuhan pengetikan sehari-hari, sehingga sebuah berkas JavaScript dapat diberi tipe lengkap tanpa mengganti namanya. Tipe yang dinyatakan lewat JSDoc juga mudah dipindahkan, karena setiap anotasinya memiliki padanan langsung pada sintaks TypeScript. Ketika berkas akhirnya diubah menjadi .ts, isi anotasinya cukup dipindahkan ke dalam sintaks TypeScript tanpa penalaran ulang.

KebutuhanJSDocTypeScript
Tipe parameter@param {number} nilainilai: number
Tipe kembalian@returns {number}: number
Tipe variabel@type {string[]}const daftar: string[]
Tipe bentuk objek@typedefinterface atau type
Impor tipe dari berkas lain@type {import("./modul.js")}import type

Fungsi yang telah diberi tipe lewat JSDoc dapat langsung dipakai dari berkas TypeScript mana pun, termasuk dari luar folder proyeknya. Berkas berikut memanggil rataRata dari proyek-jsdoc dan lolos pemeriksaan karena argumennya sesuai kontrak.

ts pemakaian-rata-rata.ts
// tidak-dijalankan
import { rataRata } from "./proyek-jsdoc/statistik.js";

console.log(rataRata([8, 7, 9]));
bash
# menjalankan berkas pemanggil dari folder proyeknya
node --no-warnings pemakaian-rata-rata.ts
text Keluaran
8

Pola kerja ini menempatkan JSDoc sebagai jembatan, bukan tujuan akhir. Berkas yang paling penting dianotasi lebih dahulu, pemeriksaan checkJs dijalankan untuk menangkap kesalahan yang sudah ada, dan berkas yang telah stabil diubah menjadi TypeScript pada waktunya sendiri. Pada setiap tahap, program tetap dapat dijalankan dan pemeriksaan tipe tetap berjalan.

Kesalahan umum#

Kesalahan berikut sering muncul saat berdampingan dengan kode JavaScript lama.

  • Menganggap program yang berhasil dijalankan sebagai bukti bahwa tipenya sudah benar. Alat eksekusi seperti Node.js hanya mengeksekusi isi berkas, sehingga kesalahan tipe tetap lolos sampai tsc dijalankan.
  • Menyalakan checkJs untuk seluruh proyek sekaligus pada hari pertama. Ratusan error lama yang muncul bersamaan membuat tim cenderung mematikan opsi itu lagi, padahal penerapan per folder lebih mudah dipertahankan dalam jangka panjang.
  • Membiarkan anotasi JSDoc tidak sesuai dengan isi fungsinya. Kontrak yang basi lebih berbahaya daripada tanpa kontrak, karena compiler menegakkan tipe yang sudah tidak benar.
  • Menulis @param {any} sebagai jalan pintas agar pesan error tidak muncul lagi. Pilihan ini menonaktifkan pemeriksaan pada titik itu dan menyembunyikan kesalahan dari seluruh pemanggilnya.

Ringkasan#

  • Opsi allowJs menyertakan berkas JavaScript ke dalam program, sehingga modul tanpa tipe dapat dipanggil dari TypeScript dengan parameter yang disimpulkan sebagai any.
  • Opsi checkJs melaporkan error di dalam berkas JavaScript, dan anotasi JSDoc seperti @param serta @returns membuat laporannya sama kuat dengan pemeriksaan TypeScript.
  • Tipe JSDoc berpindah langsung ke sintaks TypeScript saat berkas diubah namanya, sehingga anotasi yang ditulis sekarang tetap berguna setelah konversi.
  • Adopsi bertahap berjalan dengan menganotasi berkas terpenting lebih dahulu, memeriksa dengan checkJs, lalu mengonversi berkas yang stabil pada waktunya sendiri.

Referensi#