Jawaban Singkat / Key Takeaways: isolatedDeclarations TypeScript 5.5+ memaksa tiap file di-compile sebagai unit independen. Efek sampingnya brutal: declare module '*.css', interface Window augmentation, dan declare global yang tersebar di banyak file bisa lenyap dari output .d.ts. Workaround-nya: bundling semua global augmentation ke satu file global.d.ts — strategi yang terdengar kontradiktif dengan semangat “isolasi” tapi justru jadi satu-satunya cara menghindari silent declaration loss.
Saat TypeScript Tiba-tiba “Lupa” Global Typemu Sendiri
Bayangin: kamu maintain library React component library. Di project-mu ada file global.d.ts yang nambahin properti theme ke Window. Ada juga css.d.ts yang declare module *.module.css. Semua jalan mulus sejak TypeScript 4.x. Lalu kamu upgrade ke TypeScript 5.6, nyalakan isolatedDeclarations: true, dan publish. Seminggu kemudian consumer kirim issue: “Properti window.theme undefined di tipe.” Kamu cek dist/. File .d.ts-nya nggak mengandung augmentasi global sama sekali. Ini bukan bug. Ini konsekuensi arsitektur yang jarang dibahas.
Masalah ini menyerang library author dan plugin author yang terbiasa memanfaatkan TypeScript global augmentation. Sebelum isolatedDeclarations, tsc bisa “melihat” deklarasi di file lain dan menggabungkannya. Sekarang? Setiap file adalah pulau sendiri. Augmentasi yang terpisah tidak akan disatukan.
Cara Kerja isolatedDeclarations dan Kenapa Global Augmentation Jadi Korban
Mode declaration: true tradisional menggunakan full type checker untuk emit .d.ts. Saat file A mendeklarasikan declare module '*.css' dan file B mengimpornya, tsc tahu keduanya milik program yang sama. Declaration merging berfungsi natural. Hasilnya: output .d.ts mengandung semua augmentasi.
isolatedDeclarations membalik logika ini. Tiap file di-emit sebagai unit independen tanpa akses ke type graph global. Mekanismenya seperti ini:
- No cross-file type resolution saat emit. File A tidak bisa melihat
declare globaldari file B - Declaration merging gagal. Dua file yang sama-sama augment
Windowtidak akan disatukan di output - Ambient module declaration rawan lenyap.
declare module '*.svg'yang terpisah dari file konsumen tidak akan ikut ter-emit
Intinya: augmentation yang tadinya berfungsi via implicit merging sekarang butuh deklarasi yang self-contained di tiap file output. Dan ini nyaris mustahil kalau deklarasi-mu terpencar di banyak file.
Tiga Skenario yang Paling Sering Jadi Korban
1. Ambient Module Declarations (declare module ‘*.css')
Pattern paling umum di project React/Next.js. File src/types/css.d.ts berisi:
// src/types/css.d.ts
declare module '*.module.css' {
const classes: { readonly [key: string]: string };
export default classes;
}
declare module '*.css';
Dengan isolatedDeclarations: true, file ini tetap di-emit sebagai .d.ts. Masalahnya: kalau ada file lain yang juga mendeklarasikan ambient module, atau kalau file ini di-import secara implisit via include di tsconfig.json, output-nya jadi tidak deterministik. Consumer bisa kehilangan deklarasi *.css sama sekali.
2. Global Interface Augmentation (Window, Document, dll)
Pattern ini dipakai library analytics, tema, atau plugin yang nambah properti ke objek global browser:
// src/types/window.d.ts
export {};
declare global {
interface Window {
__THEME__: 'light' | 'dark';
__ANALYTICS__: { track: (event: string) => void };
}
}
Dengan mode tradisional, augmentasi ini melebur ke interface Window yang didefinisikan lib.dom.d.ts. Tapi dengan isolatedDeclarations, kalau library consumer punya skipLibCheck: true (sangat umum), deklarasi ini bisa tidak terlihat karena tsc tidak membaca .d.ts upstream saat emit.
3. Module Augmentation dari Package Eksternal
Skenario paling parah: kamu meng-augment module dari node_modules.
// src/types/express.d.ts
import 'express';
declare module 'express' {
interface Request {
userId: string;
session: SessionData;
}
}
Ini 100% bergantung pada declaration merging lintas file. Begitu isolatedDeclarations aktif, augmentasi ini hanya berlaku di file yang mengimpornya secara eksplisit. Kalau consumer lain tidak import express.d.ts, properti userId dan session lenyap dari tipe Request.
Workaround Terbukti: Strategi Bundling global.d.ts
Ini inti dari artikel ini. Solusinya sederhana tapi membutuhkan perubahan mindset: kumpulkan semua global augmentation dan ambient module declaration ke satu file global.d.ts. Jangan biarkan deklarasi ini terpencar di banyak file.
Kedengarannya kontradiktif, kan? Fitur ini namanya isolated declarations, tapi solusinya justru nge-bundle semuanya. Ini karena isolatedDeclarations hanya mengisolasi proses emit, bukan type checking. File global.d.ts yang self-contained akan di-emit secara independen dan tetap lengkap karena semua deklarasi ada di satu tempat.
// src/global.d.ts — SATU FILE UNTUK SEMUA GLOBAL AUGMENTATION
// Jangan ekspor apapun. Biarkan file ini jadi script (bukan module).
// Ambient CSS modules
declare module '*.module.css' {
const classes: { readonly [key: string]: string };
export default classes;
}
declare module '*.css';
declare module '*.svg' {
import React from 'react';
const SVG: React.FC<React.SVGProps<SVGSVGElement>>;
export default SVG;
}
// Window augmentation
interface Window {
__THEME__: 'light' | 'dark';
__ANALYTICS__: { track: (event: string) => void };
}
// Module augmentation external
declare module 'express' {
interface Request {
userId: string;
session: import('express-session').SessionData;
}
}
Aturan kunci:
- File
global.d.tsharus menjadi script, bukan module. Artinya: jangan adaimportatauexportdi level top. Kalau kamu butuh tipe dari package eksternal, pakaiimport()type syntax di dalam deklarasi (seperti contohexpress-sessiondi atas). - Letakkan file ini di root
src/dan pastikan masuk dalamincludeditsconfig.json. - Jangan pisahkan ambient module declaration ke file terpisah seperti
css.d.ts,svg.d.ts,window.d.ts. Semua jadi satu.
Konfigurasi tsconfig.json yang Mendukung Strategi Ini
// tsconfig.json
{
"compilerOptions": {
"declaration": true,
"isolatedDeclarations": true,
"outDir": "dist",
"rootDir": "src"
// Jangan pakai composite: true di sini
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/global.d.ts"],
"exclude": ["node_modules", "dist"]
}
Pastikan global.d.ts eksplisit di include. Kalau cuma mengandalkan wildcard src/**/*.ts, file .d.ts tetap masuk, tapi eksplisit lebih aman untuk dokumentasi intent.
Mengapa Strategi Ini Bekerja (dan Alternatif Lain Gagal)
Kamu mungkin berpikir: “Kenapa nggak pakai /// <reference path="..." /> aja?” atau “Kenapa nggak import global.d.ts di setiap file?” Dua pendekatan ini gagal di isolatedDeclarations karena:
- Triple-slash directives (
/// <reference />) diabaikan saat emit isolated. Tsc tidak mengikuti referensi ke file lain untuk mengumpulkan deklarasi. - Import eksplisit (misalnya
import '../types/express') hanya membawa tipe untuk file yang mengimpor. File lain yang tidak mengimpor tetap tidak melihat augmentasi. Ini menciptakan inkonsistensi: tipeRequestpunyauserIddi satu file, tapi tidak di file lain.
Satu-satunya cara yang konsisten adalah memastikan deklarasi global selalu self-contained di satu file yang tidak memerlukan merging dengan file lain. Itulah global.d.ts monolitik.
Verifikasi: Pastikan Global Typemu Tidak Hilang
Sebelum publish package, jalankan langkah verifikasi ini. Gue pakai checklist ini untuk semua library yang gue maintain sejak migrasi ke isolatedDeclarations:
- Build dengan
--declaration --isolatedDeclarationsdan periksa folderdist/. Apakahglobal.d.tsmuncul sebagai output? Kalau tidak, periksaincludedi tsconfig. - Buka
dist/global.d.ts. Apakah semua ambient module dan augmentation ada? Jangan percaya otomatis; buka manual. - Buat project consumer kosong yang mengimpor package-mu. Coba akses properti global yang kamu augmentasi (misalnya
window.__THEME__). Kalau TypeScript error, deklarasi-mu hilang. - Jalankan
npx attw(Are The Types Wrong) untuk memvalidasi type resolution package-mu dari perspektif consumer. - Test dengan
skipLibCheck: truedi project consumer. Banyak developer pakai setting ini, dan ini kondisi paling rentan untuk kehilangan global type.
Checklist Migrasi untuk Package Author
Kalau library-mu sudah ada dan sedang upgrade ke TypeScript 5.6 dengan isolatedDeclarations, ikuti checklist ini:
- Audit semua file
.d.tsdi project. Identifikasi yang mengandungdeclare module,declare global, atauinterface Window. - Buat file
src/global.d.tsdan pindahkan semua deklarasi global ke sana. Jangan ada satupun yang ketinggalan. - Hapus ambient declaration dari file-file aslinya yang sudah dipindahkan. File
css.d.tsdanwindow.d.tsyang lama bisa dihapus total. - Verifikasi tidak ada
import/exportdi level topglobal.d.ts. File ini harus script, bukan module. - Update
tsconfig.json: pastikanincludemencakupglobal.d.tssecara eksplisit. - Build dan verifikasi output seperti langkah di atas.
- Naikkan major version karena ini berpotensi breaking change bagi consumer yang menggantungkan type dari file-file yang kamu hapus.
Baca juga: Benchmark isolatedDeclarations di monorepo 120 package untuk lihat seberapa besar impact build speed setelah migrasi. Dan jangan lewatkan panduan konfigurasi exports package.json biar consumer nggak kena module resolution error.
FAQ
Apakah isolatedDeclarations wajib dipakai di TypeScript 5.6?
Tidak wajib. isolatedDeclarations adalah fitur opsional yang bisa kamu nyalakan via tsconfig.json. Tapi TypeScript team memberi sinyal bahwa di masa depan ini bisa menjadi default. Library author disarankan mulai beradaptasi sekarang. Referensi: TypeScript 5.5 Release Blog.
Apakah cukup pakai reference directive daripada bundling global.d.ts?
Tidak cukup. /// <reference path="..." /> tidak diikuti saat emit isolatedDeclarations. Tsc tidak akan mengumpulkan deklarasi dari file referensi. Satu-satunya cara yang handal adalah menggabungkan semua dalam satu file self-contained. Lihat dokumentasi triple-slash directives untuk detail keterbatasannya.
Apakah consumer perlu melakukan perubahan setelah library author migrasi ke global.d.ts?
Tidak, kalau migrasi dilakukan dengan benar. Consumer tetap mengimpor package seperti biasa. Global augmentation akan tersedia otomatis karena sudah terkandung dalam .d.ts yang di-emit. Masalah hanya muncul kalau author gagal memigrasikan deklarasi dengan benar, sehingga global type hilang dari output.
Apakah strategi ini berpengaruh ke performa build?
Tidak signifikan. File global.d.ts monolitik tetap di-emit sebagai satu unit terisolasi. Justru performa build bisa lebih baik karena tidak ada inter-dependency antar declaration file. Untuk data benchmark lengkap, cek artikel benchmark isolatedDeclarations.
Kesimpulan
isolatedDeclarations membawa peningkatan build performance yang signifikan, tapi ada harga yang harus dibayar: global augmentation dan ambient module declaration yang tersebar di banyak file akan lenyap dari output .d.ts. Strategi bundling ke global.d.ts adalah solusi yang terbukti efektif. Satu file, self-contained, tanpa import/export top-level, dan semuanya survive proses emit terisolasi.
Jangan tunggu consumer kirim issue “type hilang”. Audit .d.ts project-mu sekarang. Identifikasi semua declare module dan declare global. Gabungkan jadi satu. Test di project consumer kosong. Fix ini kecil, tapi dampaknya besar buat reputasi package-mu di komunitas TypeScript.
Referensi: TypeScript 5.5 Release Blog, isolatedDeclarations TSConfig Documentation, Are The Types Wrong.
