Resolusi Modul

Mode bundler dan node16 serta pemakaian paths dan alias.

Daftar isi

Setiap kali compiler menemukan pernyataan import, ia mencari berkas yang dirujuk specifier-nya sebelum tipe berkas itu dapat diperiksa. Topik ini membahas dua mode resolusi modul yang paling sering dipakai, yaitu nodenext yang meniru aturan Node.js dan bundler yang meniru perilaku alat bundler, beserta opsi paths untuk membuat alias jalur. Pembahasan memakai tiga proyek kecil dengan konfigurasi berbeda, sehingga perbedaan perilakunya dapat dibandingkan secara langsung. Setelah membacanya, mode resolusi dapat dipilih sesuai lingkungan eksekusi program, dan alias dapat ditulis sambil mengetahui batasannya.

Dua mode resolusi modul#

Opsi moduleResolution menentukan aturan yang dipakai compiler untuk menerjemahkan specifier menjadi berkas di disk. Mode node16 dan nodenext meniru perilaku Node.js modern, termasuk aturan yang mewajibkan ekstensi berkas pada pernyataan impor ESM, sedangkan mode bundler meniru alat penggabung berkas yang menyelesaikan ekstensi itu sendiri. Opsi module mengikuti mode yang dipilih, misalnya "module": "nodenext" yang otomatis memakai resolusi nodenext. Perbedaan paling terlihat antara keduanya dirangkum pada tabel berikut.

Perilakunodenextbundler
Specifier relatif tanpa ekstensi pada impor ESMditolakditerima
Specifier berakhiran .tsdengan allowImportingTsExtensionsdengan allowImportingTsExtensions
Lingkungan yang ditiruNode.jsalat bundel seperti esbuild dan Vite

Pilihan mode sebaiknya mengikuti alat yang benar-benar menjalankan program, bukan soal selera. Program yang dieksekusi Node.js secara langsung memakai mode nodenext, sedangkan aplikasi yang selalu melewati esbuild atau Vite memakai mode bundler. Mode yang keliru membuat pemeriksaan tipe menyetujui kode yang justru gagal saat dijalankan, atau sebaliknya menolak kode yang sebenarnya sah.

Ekstensi wajib pada mode nodenext#

Proyek proyek-node/ menyimulasikan proyek Node.js modern. Berkas package.json menyatakan "type": "module", sehingga berkas .ts di dalamnya diperlakukan sebagai modul ESM, dan tsconfig.json memakai mode nodenext. Berkas satuan.ts mengekspor satu fungsi konversi suhu, sedangkan laporan-salah.ts mengimpornya tanpa menulis ekstensi.

json proyek-node/package.json
{
  "name": "proyek-node",
  "private": true,
  "type": "module"
}
json proyek-node/tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noEmit": true,
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "target": "es2022",
    "allowImportingTsExtensions": true
  }
}
ts proyek-node/satuan.ts
export function celsiusKeFahrenheit(celsius: number): number {
  return (celsius * 9) / 5 + 32;
}
ts proyek-node/laporan-salah.ts
// tidak-dijalankan
import { celsiusKeFahrenheit } from "./satuan";

console.log("25 C =", celsiusKeFahrenheit(25), "F");
bash
# memeriksa proyek dengan mode nodenext
tsc --project proyek-node --pretty false
text Keluaran
proyek-node/laporan-salah.ts(2,37): error TS2835: Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './satuan.js'?

Pesan TS2835 menyatakan aturan mode nodenext secara harfiah, yaitu specifier relatif pada impor ESM wajib mencantumkan ekstensi berkasnya. Satu-satunya pesan menunjuk laporan-salah.ts, sedangkan berkas lain lolos tanpa keluhan. Node.js menuntut ekstensi itu karena modul ESM diselesaikan satu per satu saat berjalan, tanpa mekanisme menebak ekstensi seperti pada sistem modul lama.

Versi yang benar hanya menuliskan ekstensi secara lengkap. Berkas laporan.ts memakai specifier ./satuan.ts, dan pemeriksaan yang sama tidak melaporkan apa pun tentangnya, sehingga program langsung dapat dijalankan.

ts proyek-node/laporan.ts
// tidak-dijalankan
import { celsiusKeFahrenheit } from "./satuan.ts";

console.log("25 C =", celsiusKeFahrenheit(25), "F");
bash
# menjalankan versi yang benar dari dalam folder proyeknya
( cd proyek-node && node laporan.ts )
text Keluaran
25 C = 77 F

Mode bundler menerima specifier tanpa ekstensi#

Proyek proyek-bundler/ memakai specifier tanpa ekstensi yang sama dengan laporan-salah.ts sebelumnya, tetapi konfigurasinya kini memilih mode bundler. Berkas satuan.ts di dalamnya menyalin isi berkas pada proyek sebelumnya, sedangkan laporan.ts mengimpornya tanpa ekstensi. Pemeriksaan compiler terhadap proyek ini berakhir tanpa satu pun pesan.

json proyek-bundler/tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noEmit": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "target": "es2022"
  }
}
ts proyek-bundler/laporan.ts
// tidak-dijalankan
import { celsiusKeFahrenheit } from "./satuan";

console.log("25 C =", celsiusKeFahrenheit(25), "F");
bash
# memeriksa proyek dengan mode bundler
tsc --project proyek-bundler --pretty false

Mode bundler berarti compiler meniru cara esbuild menyelesaikan specifier, sehingga kesesuaian antara pemeriksaan tipe dan alat build terjaga. Kesepakatan itu bermakna hanya bila program benar-benar melewati bundler, karena Node.js sendiri tidak menyelesaikan specifier tanpa ekstensi. Perintah berikut membundle kedua berkas dengan esbuild lalu menjalankan hasilnya, sehingga jalur eksekusi yang diasumsikan konfigurasi ini terbukti benar-benar dapat dijalankan.

bash
# membundle proyek di folder sementara lalu menjalankan hasilnya
rm -rf /tmp/rm-bundler && mkdir -p /tmp/rm-bundler
cp proyek-bundler/laporan.ts proyek-bundler/satuan.ts /tmp/rm-bundler/
( cd /tmp/rm-bundler && esbuild laporan.ts --bundle --outfile=bundle.js --log-level=silent && node bundle.js )
text Keluaran
25 C = 77 F

Alias jalur dengan paths#

Opsi paths memetakan awalan specifier ke jalur di dalam proyek, sehingga kode dapat mengimpor dengan nama yang pendek tanpa rangkaian ../ yang panjang. Proyek proyek-alias/ memetakan @satuan/* ke folder src/, sehingga @satuan/satuan menunjuk berkas src/satuan.ts yang isinya sama dengan proyek-proyek sebelumnya. Pemetaan ini hanya dikenali compiler, dan pemeriksaan terhadap proyek ini berakhir tanpa pesan.

json proyek-alias/tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noEmit": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "target": "es2022",
    "paths": {
      "@satuan/*": ["./src/*"]
    }
  }
}
ts proyek-alias/src/laporan.ts
// tidak-dijalankan
import { celsiusKeFahrenheit } from "@satuan/satuan";

console.log("25 C =", celsiusKeFahrenheit(25), "F");
bash
# memeriksa proyek dengan pemetaan paths
tsc --project proyek-alias --pretty false
bash
# menjalankan berkas yang mengimpor alias tanpa dukungan runtime
( cd proyek-alias && node src/laporan.ts 2>&1 | grep -m1 "Cannot find package" )
text Keluaran
Error [ERR_MODULE_NOT_FOUND]: Cannot find package '@satuan/satuan' imported from /home/kal/my-project/site/content/typescript/modul-dan-perkakas/resolusi-modul/proyek-alias/src/laporan.ts

Pemeriksaan tipe lolos karena compiler membaca paths, sedangkan eksekusi langsung gagal karena Node.js tidak membaca opsi tersebut. Pesan error tersebut memperlakukan @satuan/satuan sebagai nama package yang tidak ditemukan, bukan sebagai berkas yang salah tulis. Agar program benar-benar berjalan, pemetaan yang sama harus dinyatakan pada alat yang menjalankannya, misalnya opsi alias milik esbuild atau resolve.alias milik Vite.

Kesalahan umum#

Kesalahan berikut sering muncul saat memilih mode resolusi dan menulis alias.

  • Memakai mode bundler untuk program yang dijalankan Node.js secara langsung. Pemeriksaan tipe lolos, tetapi eksekusi gagal mencari berkasnya karena specifier tanpa ekstensi tidak pernah diselesaikan oleh Node.js.
  • Menganggap paths berlaku pada semua alat. Opsi ini hanya dibaca compiler, sehingga pemetaan yang sama harus ditulis ulang di konfigurasi bundler, runner pengujian, maupun alat lain yang memproses specifier.
  • Menulis nilai pemetaan paths tanpa awalan ./, misalnya "src/*". TypeScript 7 menolaknya dengan pesan TS5090, yaitu nilai paths yang tidak relatif tidak diizinkan, sehingga awalan ./ wajib ditulis.
  • Melupakan "type": "module" pada package.json proyek Node.js. Tanpa deklarasi itu, berkas .ts dianggap CommonJS, sehingga aturan ekstensi khas ESM yang dituntut mode nodenext tidak sepenuhnya berlaku.

Ringkasan#

  • Opsi moduleResolution menentukan aturan penerjemahan specifier menjadi berkas, dengan nodenext yang meniru Node.js dan bundler yang meniru alat penggabung berkas.
  • Mode nodenext mewajibkan ekstensi pada specifier relatif impor ESM melalui pesan TS2835, sedangkan mode bundler menerima specifier tanpa ekstensi.
  • Mode bundler bermakna hanya bila program benar-benar melewati bundler, karena Node.js tidak menyelesaikan ekstensi untuk siapa pun.
  • Opsi paths membuat alias yang hanya dikenali compiler, sehingga pemetaan yang sama wajib dinyatakan pada alat yang menjalankan program agar eksekusinya berhasil.

Referensi#