Mengonversi seluruh proyek JavaScript ke TypeScript dalam satu langkah jarang menjadi pilihan yang masuk akal, karena program harus tetap berjalan selagi strukturnya berubah. Topik ini menyusun strategi konversi bertahap pada proyek kecil yang berjalan, mulai dari menerima berkas JavaScript apa adanya, menyalakan pemeriksaan, memberi kontrak lewat JSDoc, mengganti nama berkas, sampai menyalakan strict. Setiap tahap memakai tsconfig.json sendiri dan dijalankan dengan keluaran asli, sehingga akibat setiap keputusan terlihat langsung. Setelah membacanya, urutan konversi untuk proyek yang sedang berjalan dapat disusun tanpa menebak.
Titik awal proyek JavaScript#
Proyek pada topik ini berupa pencatat pembacaan suhu yang berisi dua modul CommonJS yang saling memanggil. Modul pembacaan.js menyediakan dua fungsi pengolah daftar angka, sedangkan laporan.js menyusun baris laporan dari hasilnya. Satu baris pemanggilannya sengaja memakai daftar string, dan bug kecil semacam itu lazim dijumpai pada proyek JavaScript yang sudah berjalan lama.
function tambahPembacaan(daftar, suhu) {
daftar.push(suhu);
return daftar;
}
function rataRataSuhu(daftar) {
let total = 0;
for (const suhu of daftar) {
total += suhu;
}
return total / daftar.length;
}
module.exports = { tambahPembacaan, rataRataSuhu };// tidak-dijalankan
const { rataRataSuhu } = require("./pembacaan.js");
function formatLaporan(daftar) {
return "rata-rata suhu: " + rataRataSuhu(daftar).toFixed(1);
}
const pembacaan = [20, 22, 21];
console.log(formatLaporan(pembacaan));
console.log(formatLaporan(["dua puluh"]));Konfigurasi tahap pertama hanya menyalakan allowJs dan noEmit, sedangkan strict masih dimatikan.
{
"compilerOptions": {
"allowJs": true,
"noEmit": true,
"strict": false,
"module": "commonjs",
"target": "es2022"
}
}# memeriksa proyek JavaScript dengan allowJs
tsc --project proyek-tahap-awal --pretty falseOpsi allowJs membuat compiler menerima kedua berkas JavaScript sebagai bagian dari proyek, sedangkan noEmit menjaga pemeriksaan ini tidak menghasilkan berkas baru. Pemeriksaan pertama berakhir tanpa pesan, dan keadaan itu memang tujuan tahapnya, yaitu compiler berjalan di atas proyek apa adanya tanpa mengubah satu baris kode. Fondasi tanpa pesan error ini penting karena seluruh tahap berikutnya dibangun di atasnya.
# menjalankan program pada keadaan awalnya
node proyek-tahap-awal/laporan.jsrata-rata suhu: 21.0
rata-rata suhu: NaNProgramnya memperlihatkan bug yang belum tersentuh. Pemanggilan kedua mengirim daftar string ke fungsi pengolah angka, sehingga penjumlahannya menghasilkan NaN dan laporannya ikut rusak. Kesalahan semacam ini baru terlihat saat program dijalankan, dan menghilangkannya dengan pemeriksaan tipe menjadi tugas tiga tahap berikutnya.
Menyalakan pemeriksaan pada berkas JavaScript#
Tahap berikutnya menyalakan checkJs tanpa menyentuh satu baris kode pun. Folder proyek-tahap-checkjs memuat salinan identik kedua berkas JavaScript pada tahap sebelumnya, dan perbedaannya hanya ada pada konfigurasinya.
{
"compilerOptions": {
"allowJs": true,
"checkJs": true,
"noEmit": true,
"strict": false,
"module": "commonjs",
"target": "es2022"
}
}proyek-tahap-checkjs/
├── pembacaan.js salinan identik dari tahap sebelumnya
├── laporan.js salinan identik dari tahap sebelumnya
└── tsconfig.json# memeriksa proyek dengan checkJs yang menyala
tsc --project proyek-tahap-checkjs --pretty falsePemeriksaan kini berjalan di dalam berkas JavaScript, tetapi laporannya masih kosong. Sebabnya ada pada dua hal, yaitu parameter tanpa anotasi yang diperlakukan any dan opsi strict yang masih dimatikan, sehingga pemanggilan salah sasaran pun lolos. Tahap ini sengaja dibuat ringan, karena tujuannya membiasakan pemeriksaan berjalan pada setiap build sebelum laporannya diperketat.
Laporan yang kosong pada tahap ini tetap membawa manfaat yang nyata. Pemeriksaan yang telah menyala akan mulai melaporkan kesalahan sintaks dan pemakaian yang pasti salah segera setelah kode baru masuk, sehingga proyek tidak lagi menerima tambalan rusak tanpa peringatan. Kebiasaan menjalankan tsc pada setiap build juga terbentuk di saat laporannya masih tenang, bukan di saat ratusan error menumpuk. Tahap yang ringan dengan demikian membangun kebiasaan yang dibutuhkan pada tahap-tahap berikutnya.
Memberi kontrak lewat JSDoc#
Kontrak tipe diberikan lebih dahulu pada modul yang paling sering dipakai, dan pada proyek ini modulnya adalah pembacaan.js beserta fungsi lapornya. Anotasi JSDoc dipilih karena berkasnya tetap JavaScript, sehingga program berjalan persis seperti sebelumnya selagi kontraknya terpasang. Konfigurasinya sama dengan tahap sebelumnya karena checkJs sudah menyala.
/**
* Menambahkan satu pembacaan suhu ke daftar.
* @param {number[]} daftar
* @param {number} suhu
* @returns {number[]}
*/
function tambahPembacaan(daftar, suhu) {
daftar.push(suhu);
return daftar;
}
/**
* Menghitung rata-rata suhu dari daftar pembacaan.
* @param {number[]} daftar
* @returns {number}
*/
function rataRataSuhu(daftar) {
let total = 0;
for (const suhu of daftar) {
total += suhu;
}
return total / daftar.length;
}
module.exports = { tambahPembacaan, rataRataSuhu };// tidak-dijalankan
const { rataRataSuhu } = require("./pembacaan.js");
/**
* Menyusun satu baris laporan suhu.
* @param {number[]} daftar
* @returns {string}
*/
function formatLaporan(daftar) {
return "rata-rata suhu: " + rataRataSuhu(daftar).toFixed(1);
}
const pembacaan = [20, 22, 21];
console.log(formatLaporan(pembacaan));
console.log(formatLaporan(["dua puluh"]));# memeriksa proyek dengan kontrak JSDoc yang terpasang
tsc --project proyek-tahap-jsdoc --pretty falseproyek-tahap-jsdoc/laporan.js(15,28): error TS2322: Type 'string' is not assignable to type 'number'.Anotasi @param {number[]} pada formatLaporan langsung menangkap pemanggilan yang salah. Pesan errornya menunjuk elemen string pada argumen yang menurut kontrak harus berupa number, dan bug yang selama ini hanya terlihat sebagai NaN kini tertangkap sebelum program berjalan. Urutannya layak dicatat, yaitu modul yang paling banyak pemanggilnya dianotasi lebih dahulu agar satu kontrak menutup pemanggil terbanyak.
Batasan tahap ini juga perlu dipegang agar kemajuannya terukur. Tidak semua berkas perlu dianotasi sekarang, sebab anotasi pada modul yang jarang dipakai hanya menambah pekerjaan tanpa menutup banyak pemanggil. Satu suntingan kecil pada kontrak sudah memengaruhi seluruh pemanggilnya, sehingga penambahan anotasi dapat dilakukan sedikit demi sedikit tanpa menunggu jeda besar. Program tetap dijalankan seperti biasa sepanjang tahap ini, dan jaminan ini tidak boleh hilang.
Mengganti nama berkas dan menyalakan strict#
Berkas yang kontraknya stabil siap menjadi berkas TypeScript. Konversinya mekanis, yaitu isi anotasi JSDoc dipindah ke tanda tangan fungsi, module.exports diganti kata kunci export, dan ekstensi berkasnya diganti .ts. Folder proyek-tahap-rename memuat hasil konversi tersebut beserta pemanggilannya yang sudah diperbaiki, sedangkan pemanggilan dengan daftar string yang ditolak compiler diganti pemanggilan yang sah.
export function tambahPembacaan(daftar: number[], suhu: number): number[] {
daftar.push(suhu);
return daftar;
}
export function rataRataSuhu(daftar: number[]): number {
let total = 0;
for (const suhu of daftar) {
total += suhu;
}
return total / daftar.length;
}// tidak-dijalankan
import { tambahPembacaan, rataRataSuhu } from "./pembacaan.js";
function formatLaporan(daftar: number[]): string {
return "rata-rata suhu: " + rataRataSuhu(daftar).toFixed(1);
}
const pembacaan = tambahPembacaan([20, 22], 21);
console.log(formatLaporan(pembacaan));
console.log(formatLaporan([20]));Opsi strict menyala penuh pada tahap ini, dan opsi noEmit dihapus agar hasil kompilasinya dapat dijalankan.
{
"compilerOptions": {
"strict": true,
"module": "commonjs",
"target": "es2022"
}
}# mengompilasi proyek TypeScript lalu menjalankan hasilnya
tsc --project proyek-tahap-rename && node proyek-tahap-rename/laporan.jsrata-rata suhu: 21.0
rata-rata suhu: 20.0Opsi strict menyala tanpa menghasilkan satu pun error baru karena urutan tahapnya benar. Kontrak yang dibangun lewat JSDoc menjadi anotasi bahasa tanpa penalaran ulang, sehingga compiler yang kini ketat menemukan kode yang sudah sah. Program hasil kompilasinya berjalan dengan laporan yang benar, dan bug NaN pada tahap awal tidak mungkin muncul lagi karena pemanggilannya ditolak compiler sebelum sempat berjalan.
Keadaan campuran juga tetap sah pada tahap ini, karena impor yang berakhiran .js tetap diresolusi ke berkas .ts dengan nama yang sama. Konversi satu berkas dengan demikian tidak menuntut perubahan apa pun pada pemanggilnya, dan sisa berkas JavaScript dapat mengikuti pada waktunya masing-masing.
Urutan keputusan migrasi#
Urutan tahap di atas bukan kebetulan, melainkan tangga yang menjaga program tetap berjalan pada setiap anak tangganya. Tabel berikut merangkum keputusan yang diambil beserta hasilnya.
| Tahap | Keputusan | Yang diperoleh |
|---|---|---|
| 1 | allowJs menyala, strict mati | Compiler menerima proyek apa adanya |
| 2 | checkJs menyala | Pemeriksaan berjalan di dalam berkas JavaScript |
| 3 | JSDoc pada modul inti | Kontrak yang menutup pemanggil terbanyak |
| 4 | Konversi ke .ts, strict menyala | Anotasi penuh dan pemeriksaan yang ketat |
Tangga ini menentukan urutan berkas, bukan hanya urutan opsi. Berkas yang paling jarang berubah dan paling sedikit pemanggilnya dikonversi terakhir karena manfaat kontraknya paling kecil, sedangkan modul inti dianotasi lebih dahulu. Pada proyek besar, checkJs dapat disertakan per folder terlebih dahulu sehingga laporannya tumbuh seiring wilayah yang dianotasi. Keadaan campuran berupa sebagian berkas .ts dan sebagian .js beranotasi adalah keadaan normal yang bisa bertahan lama, karena setiap tahapnya menghasilkan program yang berjalan dan pemeriksaan yang tetap menyala.
Kesalahan umum#
Kesalahan berikut sering muncul saat mengonversi proyek JavaScript yang sedang berjalan.
- Mengonversi seluruh berkas menjadi
.tssekaligus pada hari pertama. Semua anotasi harus ditulis terlebih dahulu, dan program berisiko rusak tanpa jalur kembali yang jelas. Perbaikannya berupa mengikuti tanggaallowJs,checkJs, JSDoc, lalu konversi per berkas. - Menyalakan
strictsebelum anotasi selesai. Compiler melaporkan parameter tanpa tipe pada seluruh berkas lama sekaligus, dan laporan yang menumpuk mendorong opsi itu dimatikan lagi. Perbaikannya berupa menyalakannya setelah kontrak terpasang pada modul inti. - Membiarkan anotasi JSDoc tidak cocok dengan implementasinya setelah kode berubah. Kontrak yang basi lebih menyesatkan daripada tanpa kontrak karena compiler menegakkan tipe yang sudah tidak benar. Perbaikannya berupa memperbarui kontrak bersama kodenya dalam satu suntingan.
- Menyalin
tsconfig.jsontahap akhir ke proyek lain yang belum dianotasi. Konfigurasi yang cocok untuk proyek yang sudah dikonversi memicu ratusan error pada proyek yang baru memulai. Perbaikannya berupa menyalakan opsi sesuai tahap proyeknya, bukan mengikuti tahap proyek lain.
Ringkasan#
- Migrasi yang aman mengikuti urutan
allowJs,checkJs, anotasi JSDoc pada modul inti, penggantian ekstensi berkas, lalustrictyang dinyalakan terakhir. checkJstanpa anotasi nyaris tidak melaporkan apa pun karena parameter tanpa tipe diperlakukananydanstrictmasih dimatikan.- JSDoc memberi kontrak tanpa mengubah ekstensi berkas, sehingga program tetap berjalan selagi kontraknya menangkap pemanggil yang salah.
- Konversi berkas yang stabil menjadi mekanis karena anotasinya dipindah ke tanda tangan fungsi, dan
strictyang menyala setelahnya menemukan anotasi yang sudah lengkap.
Referensi#
- Migrating from JavaScript — TypeScript Handbook — Panduan resmi langkah migrasi proyek JavaScript ke TypeScript.
- Type-Checking JavaScript Files — TypeScript Handbook — Opsi allowJs, checkJs, dan perilaku pemeriksaan berkas JavaScript.
- TSConfig Reference — typescriptlang.org — Rujukan opsi tsconfig.json yang dinyalakan pada setiap tahap migrasi.