Berkas Deklarasi

Anatomi berkas .d.ts, deklarasi ambient, dan kata kunci declare.

Daftar isi

Banyak pustaka JavaScript yang dipakai sehari-hari ditulis tanpa satu pun anotasi tipe, sehingga compiler TypeScript tidak punya informasi apa pun tentang bentuk fungsi maupun nilainya. Berkas deklarasi (declaration file) mengisi kekosongan itu, yaitu berkas berekstensi .d.ts yang memuat penjelasan tipe tanpa isi program. Topik ini membahas anatomi berkas .d.ts, kata kunci declare, dan deklarasi ambient untuk modul pihak ketiga, semuanya lewat proyek kecil yang dapat diperiksa langsung. Setelah membacanya, deklarasi untuk pustaka buatan sendiri dapat ditulis dengan benar dan pesan error yang berkaitan dengan deklarasi dapat dibaca dengan tepat.

Pustaka tanpa tipe dan deklarasi pendampingnya#

Berkas .d.ts selalu berpasangan dengan kode JavaScript yang sudah ada, baik satu berkas, satu paket, maupun seluruh folder node_modules. Isinya hanya tanda tangan tipe berupa nama fungsi, tipe parameter, dan tipe kembalian, tanpa satu pun baris implementasi. Compiler membaca pasangan ini dengan aturan yang sederhana, yaitu setiap impor "./satuan.js" yang berkas .js-nya tidak ikut diperiksa akan diarahkan ke satuan.d.ts di tempat yang sama. Ekstensi .d.ts merupakan singkatan dari declaration dan menjadi penanda bahwa berkas tersebut tidak pernah dijalankan.

Proyek proyek-satuan berisi pustaka konversi suhu JavaScript tanpa satu pun anotasi, beserta deklarasi pendampingnya. Berkas pustakanya mengekspor dua fungsi konversi melalui module.exports, sedangkan deklarasinya menuliskan ulang kedua tanda tangan itu dalam sintaks TypeScript.

js proyek-satuan/satuan.js
function konversiCelsiusKeFahrenheit(celsius) {
  return (celsius * 9) / 5 + 32;
}

function konversiFahrenheitKeCelsius(fahrenheit) {
  return ((fahrenheit - 32) * 5) / 9;
}

module.exports = { konversiCelsiusKeFahrenheit, konversiFahrenheitKeCelsius };
ts proyek-satuan/satuan.d.ts
// tidak-dijalankan
export declare function konversiCelsiusKeFahrenheit(celsius: number): number;

export declare function konversiFahrenheitKeCelsius(fahrenheit: number): number;

Anatomi berkas deklarasi ini layak dibaca baris demi baris. Kata kunci declare pada setiap barisnya menyatakan bahwa fungsi ini ada di tempat lain, sehingga compiler tidak menuntut isi fungsinya, sedangkan kata kunci export menandai fungsi yang dapat diimpor dari modul tersebut. Kombinasi export declare inilah bentuk yang paling lazim pada deklarasi modul, dan bentuk yang sama akan dijumpai pada ribuan berkas deklarasi pustaka npm.

Konfigurasi proyeknya memakai strict penuh tanpa opsi allowJs, sehingga berkas satuan.js memang tidak ikut diperiksa dan satu-satunya sumber tipe bagi compiler adalah deklarasinya.

json proyek-satuan/tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noEmit": true,
    "module": "commonjs",
    "target": "es2022",
    "esModuleInterop": true
  }
}

Berkas pemanggilnya berdiri berdampingan, yaitu main.ts dengan pemanggilan yang benar dan main-salah.ts dengan argumen yang salah sasaran.

ts proyek-satuan/main.ts
// tidak-dijalankan
import { konversiCelsiusKeFahrenheit } from "./satuan.js";

console.log(konversiCelsiusKeFahrenheit(25));
console.log(konversiCelsiusKeFahrenheit(0));
ts proyek-satuan/main-salah.ts
// tidak-dijalankan
import { konversiCelsiusKeFahrenheit } from "./satuan.js";

console.log(konversiCelsiusKeFahrenheit("dua puluh"));
bash
# memeriksa proyek tanpa menghasilkan berkas keluaran
tsc --project proyek-satuan --pretty false
text Keluaran
proyek-satuan/main-salah.ts(4,41): error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.

Pesan error di atas memuat bukti bahwa tipe terbaca dari deklarasi, bukan dari isi JavaScript. Compiler menuntut parameter berupa number, dan tipe itu hanya tertulis pada satuan.d.ts karena berkas satuan.js tidak pernah dibaca pada pemeriksaan ini. Sementara itu main.ts lolos tanpa pesan, sehingga pemanggil yang mengikuti kontrak tidak terganggu oleh pemanggil yang melanggarnya.

bash
# menjalankan program pemanggil yang benar
node --no-warnings proyek-satuan/main.ts
text Keluaran
77
32

Programnya berjalan dan menghasilkan konversi yang benar, sehingga pembagian kerja keduanya terlihat jelas. Node.js tidak membaca satuan.d.ts sama sekali saat menjalankan program, karena berkas deklarasi tidak menghasilkan kode apa pun. Deklarasi hanya dibaca oleh compiler dan language server, sedangkan perilaku program tetap ditentukan sepenuhnya oleh satuan.js.

Deklarasi dipercaya, bukan diverifikasi#

Compiler memperlakukan isi berkas .d.ts sebagai kebenaran dan tidak pernah membandingkannya dengan isi JavaScript di sebelahnya. Sifat ini membuat deklarasi mudah ditulis, tetapi sekaligus menempatkan seluruh tanggung jawab atas kebenarannya pada penulisnya. Proyek proyek-janji memperlihatkan akibatnya, yaitu pustaka label pemain yang deklarasinya keliru menulis tipe kembalian sebagai number.

js proyek-janji/papan-skor.js
function labelPemain(nomor) {
  return "P" + nomor.toString().padStart(2, "0");
}

module.exports = { labelPemain };
ts proyek-janji/papan-skor.d.ts
// tidak-dijalankan
export declare function labelPemain(nomor: number): number;

Pustakanya mengembalikan label berbentuk string seperti P07, sedangkan deklarasinya menjanjikan kembalian number. Konfigurasi proyeknya sama persis dengan proyek-satuan, dan pemanggilnya menyusun satu baris program yang menurut janji deklarasi sah.

json proyek-janji/tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noEmit": true,
    "module": "commonjs",
    "target": "es2022",
    "esModuleInterop": true
  }
}
ts proyek-janji/papan.ts
// tidak-dijalankan
import { labelPemain } from "./papan-skor.js";

const label = labelPemain(7);
console.log(label * 100);
bash
# memeriksa proyek dengan deklarasi yang keliru
tsc --project proyek-janji --pretty false

Pemeriksaan berakhir tanpa satu pun pesan, padahal janji dan implementasinya bertentangan. Compiler tidak menyentuh papan-skor.js pada pemeriksaan ini, sehingga pertentangan itu tidak pernah terlihat, dan yang diperiksanya hanyalah papan.ts terhadap janji di dalam deklarasi. Pemanggilnya sendiri sudah bertindak benar menurut kontrak yang dibacanya.

bash
# menjalankan program dengan deklarasi yang keliru
node --no-warnings proyek-janji/papan.ts
text Keluaran
NaN

Keluaran NaN baru muncul saat program berjalan. Label P07 dikalikan 100 karena deklarasi menjanjikan number, padahal label tersebut adalah string yang tidak dapat dikonversi menjadi angka, sehingga JavaScript menghasilkan NaN. Kesalahan pada deklarasi dengan demikian lebih sulit ditemukan daripada kesalahan biasa, karena tidak ada laporan yang menunjuk ke sana dan akibatnya baru terlihat jauh dari sumbernya.

Memperbaiki deklarasi yang keliru#

Perbaikannya berada sepenuhnya pada deklarasi, bukan pada pemanggilnya. Proyek proyek-janji-perbaikan memuat berkas pustaka, pemanggil, dan konfigurasinya yang identik dengan proyek sebelumnya, dan satu-satunya perubahan ada pada tipe kembalian di dalam papan-skor.d.ts.

ts proyek-janji-perbaikan/papan-skor.d.ts
// tidak-dijalankan
export declare function labelPemain(nomor: number): string;
bash
# memeriksa proyek dengan deklarasi yang sudah benar
tsc --project proyek-janji-perbaikan --pretty false
text Keluaran
proyek-janji-perbaikan/papan.ts(4,13): error TS2362: The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type.

Begitu deklarasi sesuai dengan perilaku pustakanya, compiler langsung menolak pemanggil yang mengalikan label dengan angka. Pesan errornya menunjuk baris perkalian pada papan.ts dan menjelaskan bahwa operasi aritmetika menuntut tipe bilangan. Pola ini adalah alasan utama menulis deklarasi dengan cermat, sebab kualitas seluruh pemeriksaan pada pemanggil ditentukan oleh satu berkas yang biasanya jauh lebih pendek daripada implementasinya.

Kasus seperti ini sering terjadi pada pustaka nyata karena penulis deklarasi dan pemakainya biasanya orang yang berbeda. Pemakai membaca tipe yang tersedia, menyusun kode yang sah menurut tipe itu, dan tidak pernah berkesempatan memeriksa isi implementasinya. Berkas deklarasi yang tersimpan di sebelah implementasinya dengan demikian perlu diperlakukan sebagai bagian dari kode yang wajib diperbarui, bukan lampiran yang ditulis sekali lalu dilupakan. Deklarasi yang basi biasanya lahir dari implementasi yang berubah tanpa diikuti pembaruan tipenya.

Deklarasi ambient dengan declare#

Tidak semua deklarasi berpasangan dengan berkas JavaScript di folder yang sama. Lingkungan tempat program berjalan juga menyediakan hal-hal yang tidak diimpor dari mana pun, misalnya fungsi global dari skrip yang dimuat sebelum program berjalan. Deklarasi ambient (ambient declaration) menjelaskan keberadaan itu dengan kata kunci declare, sehingga compiler mengenalinya tanpa berkas pendamping mana pun.

Berkas lingkungan.d.ts berikut memuat dua bentuk deklarasi ambient. Baris pertama menjanjikan fungsi global tampilkanSkor yang tidak berada di dalam modul apa pun, sedangkan blok berikutnya menjelaskan modul bernama pustaka-tanggal beserta fungsi formatTanggal yang diekspornya. Bentuk declare module "nama" inilah cara mendeklarasikan paket yang tidak membawa deklarasinya sendiri, dan pola yang sama dipakai paket-paket deklarasi komunitas dalam jumlah besar.

ts lingkungan.d.ts
// tidak-dijalankan
declare function tampilkanSkor(pemain: string, poin: number): void;

declare module "pustaka-tanggal" {
  export function formatTanggal(tanggal: Date, pola: string): string;
}
ts panggil.ts
// tidak-dijalankan
tampilkanSkor("Citra", 150);
bash
# memeriksa pemanggil tanpa menyertakan deklarasinya
tsc --noEmit --pretty false panggil.ts
text Keluaran
panggil.ts(2,1): error TS2304: Cannot find name 'tampilkanSkor'.
bash
# memeriksa pemanggil bersama berkas deklarasi ambientnya
tsc --noEmit --pretty false panggil.ts lingkungan.d.ts

Tanpa deklarasi, compiler menolak pemanggilan tampilkanSkor karena namanya tidak dikenal di mana pun. Setelah lingkungan.d.ts disertakan pada pemeriksaan, pemeriksaan yang sama lolos tanpa pesan karena deklarasi ambient memperluas lingkup nama yang dikenal compiler. Perhatikan bahwa tidak ada berkas implementasi yang disertakan sama sekali, sebab deklarasi ini memang tidak berpasangan dengan berkas mana pun.

Pola yang sama berlaku untuk modul. Berkas berikut mengimpor pustaka-tanggal lewat nama paketnya, persis seperti mengimpor paket dari node_modules.

ts aplikasi.ts
// tidak-dijalankan
import { formatTanggal } from "pustaka-tanggal";

const label: string = formatTanggal(new Date(2026, 0, 15), "dd-MM-yyyy");
console.log(label.length);
bash
# memeriksa impor paket tanpa deklarasi ambient modulnya
tsc --noEmit --pretty false aplikasi.ts
text Keluaran
aplikasi.ts(2,31): error TS2307: Cannot find module 'pustaka-tanggal' or its corresponding type declarations.

Impor pustaka-tanggal ditolak dengan pesan Cannot find module karena tidak ada node_modules maupun berkas lain yang dapat diresolusi, dan pemeriksaan yang sama lolos begitu lingkungan.d.ts disertakan bersamanya. Pada proyek nyata, penulisan deklarasi semacam ini dalam jumlah besar adalah tugas paket-paket deklarasi komunitas yang dipasang lewat npm.

Kesalahan umum#

Kesalahan berikut sering muncul saat menulis dan memakai berkas deklarasi.

  • Menulis implementasi di dalam berkas .d.ts. Isi berkas deklarasi hanya boleh berupa tanda tangan tipe, dan isi fungsi yang ditulis di sana ditolak compiler sebagai error. Perbaikannya berupa memindahkan implementasi ke berkas JavaScript dan menyisakan deklarasinya saja.
  • Menganggap compiler memverifikasi kecocokan deklarasi dengan implementasinya. Deklarasi yang keliru lolos tanpa satu pun pesan dan akibatnya baru terlihat saat program berjalan, sebagaimana ditunjukkan proyek proyek-janji. Perbaikannya berupa menguji deklarasi dengan pemanggil nyata.
  • Melupakan berkas deklarasi pada perintah pemeriksaan berkas tunggal. Pemeriksaan semacam ini hanya membaca berkas yang disebutkan, sehingga pemanggilan yang sebenarnya sah ditolak dengan error Cannot find name. Perbaikannya berupa menyertakan berkas deklarasinya pada perintah atau memakai tsconfig.json yang mencakup keduanya.
  • Menaruh declare module di dalam berkas yang memuat import atau export. Deklarasi pada berkas modul hanya dikenal di dalam modul itu, sehingga impor paket dari berkas lain tetap ditolak. Perbaikannya berupa memindahkan deklarasi ke berkas .d.ts global.

Ringkasan#

  • Berkas .d.ts memuat tanda tangan tipe tanpa implementasi, dan compiler memakainya untuk mengetik kode JavaScript yang tidak menyertakan anotasi apa pun.
  • Kata kunci declare menyatakan keberadaan di luar berkas, sedangkan declare module "nama" mendeklarasikan modul pihak ketiga beserta tipe ekspornya.
  • Compiler mempercayai deklarasi mentah-mentah, sehingga deklarasi yang keliru lolos pemeriksaan dan akibatnya baru terlihat saat program berjalan.
  • Runtime tidak membaca berkas deklarasi, sehingga keberadaannya tidak mengubah perilaku program sama sekali.

Referensi#