TYPESCRIPT MONOREPO LARGE-SCALE-APPLICATIONS MODULE-RESOLUTION PROJECT-REFERENCES DEVELOPER-EXPERIENCE BUILD-TOOLS CODE-ORGANIZATION TYPE-SAFETY SOFTWARE-ARCHITECTURE INCREMENTAL-BUILDS CODEBASE-MANAGEMENT

TypeScript untuk Aplikasi Skala Besar: Menguasai Module Resolution dan Project References di Monorepo

⏱️ 11 menit baca
👨‍💻

TypeScript untuk Aplikasi Skala Besar: Menguasai Module Resolution dan Project References di Monorepo

1. Pendahuluan

Sebagai developer web, kita semua tahu betapa berharganya TypeScript dalam membangun aplikasi yang robust dan mudah dirawat. Dari menangkap bug di compile-time hingga memberikan autocomplete yang cerdas di IDE, TypeScript adalah game-changer. Namun, seiring pertumbuhan codebase, terutama dalam arsitektur monorepo yang semakin populer, tantangan baru mulai muncul. Pernahkah Anda merasakan:

Jika ya, Anda tidak sendirian! Masalah-masalah ini adalah keluhan umum di proyek TypeScript skala besar. Untungnya, TypeScript memiliki dua fitur powerful yang dirancang khusus untuk mengatasi tantangan ini: Module Resolution dan Project References.

Dalam artikel ini, kita akan menyelami kedua konsep ini, memahami cara kerjanya, dan belajar bagaimana mengimplementasikannya secara efektif di monorepo Anda. Bersiaplah untuk meningkatkan performa type checking, menyederhanakan manajemen dependensi, dan membuat pengalaman pengembangan Anda jauh lebih menyenangkan! 🚀

2. Tantangan TypeScript di Aplikasi Skala Besar

Sebelum kita masuk ke solusi, mari kita pahami lebih dalam masalah yang kita hadapi:

❌ Type Checking yang Lambat

Di proyek kecil, type checking berjalan cepat. Tapi bayangkan aplikasi dengan puluhan ribu baris kode, atau monorepo dengan lusinan paket. Setiap kali Anda menyimpan file, TypeScript harus menganalisis ulang banyak bagian kode, yang bisa memakan waktu berdetik-detik, bahkan menit. Ini sangat menghambat produktivitas developer.

❌ Manajemen Dependensi yang Rumit

Dalam monorepo, Anda mungkin memiliki banyak paket yang saling bergantung. Misalnya, app-web bergantung pada ui-components, dan ui-components bergantung pada utils. Mengelola versi, memastikan type definition saling cocok, dan menghindari circular dependencies bisa menjadi mimpi buruk.

❌ Rebuild Penuh yang Tidak Efisien

Secara default, jika Anda mengubah satu file di satu paket dalam monorepo, TypeScript mungkin merasa perlu memeriksa ulang atau bahkan membangun ulang seluruh proyek yang bergantung padanya. Ini sangat tidak efisien, terutama untuk perubahan kecil.

❌ “tsconfig.json” yang Menggembung

Mencoba mengatur semua alias dan pengaturan untuk seluruh monorepo dalam satu file tsconfig.json bisa membuatnya sangat besar dan sulit dikelola.

📌 Intinya: Ketika proyek TypeScript membesar, pendekatan default bisa menjadi bottleneck yang signifikan.

3. Memahami Module Resolution di TypeScript

Module resolution adalah proses di mana TypeScript (dan Node.js) mencari file yang di-import dalam kode Anda. Ini adalah fondasi bagaimana kode Anda saling terhubung. Memahami cara kerjanya sangat penting untuk mengoptimalkan proyek skala besar.

Bagaimana TypeScript Menemukan Modul?

Secara default, TypeScript mengikuti strategi module resolution Node.js. Ketika Anda menulis import { foo } from 'bar';, TypeScript akan mencoba mencari bar di:

  1. node_modules/bar/index.ts
  2. node_modules/bar/index.tsx
  3. node_modules/bar/index.d.ts
  4. node_modules/bar.ts
  5. node_modules/bar.tsx
  6. node_modules/bar.d.ts
  7. node_modules/bar/package.json (mencari field main, types, atau exports)

Dan seterusnya, naik ke direktori induk hingga menemukan node_modules atau mencapai root.

Mengoptimalkan dengan baseUrl dan paths

Untuk proyek skala besar, terutama monorepo, seringkali Anda memiliki shared code atau modul internal yang tidak berada di node_modules. Di sinilah baseUrl dan paths di tsconfig.json berperan.

// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": ".", // Direktori dasar untuk pencarian modul
    "paths": {
      "@shared/*": ["packages/shared/*"], // Alias untuk paket shared
      "@app/*": ["src/*"] // Alias untuk modul internal aplikasi
    }
  }
}

Dengan konfigurasi di atas, Anda bisa mengimpor modul seperti ini:

// packages/app/src/index.ts
import { utilityFunction } from '@shared/utils';
import { HomePage } from '@app/pages/home';

💡 Manfaat:

Strategi Module Resolution Modern (nodenext, bundler)

TypeScript juga terus berevolusi dengan module resolution. Dengan moduleResolution: "nodenext" atau "bundler" (tersedia di TS 5.x), TypeScript lebih cerdas dalam memahami bagaimana bundler seperti Webpack atau Vite akan menyelesaikan modul, termasuk dukungan untuk package.json exports field. Ini penting untuk kompatibilitas dengan ekosistem JavaScript modern (ESM vs CommonJS).

// tsconfig.json
{
  "compilerOptions": {
    "moduleResolution": "bundler", // Atau "nodenext"
    "module": "esnext", // Atau "node16"
    "target": "esnext"
  }
}

Tips: Untuk proyek baru, pertimbangkan untuk menggunakan nodenext atau bundler untuk module resolution yang lebih akurat dan modern. Namun, perhatikan implikasi untuk bundler dan runtime Anda.

4. Revolusi dengan Project References

Project References adalah fitur yang dirancang untuk memecah proyek TypeScript besar menjadi proyek-proyek yang lebih kecil dan saling bergantung. Ini adalah kunci untuk mengatasi masalah performa dan manajemen dependensi di monorepo.

Apa itu Project References?

Bayangkan monorepo Anda memiliki tiga paket: core-library, ui-components, dan web-app.

Tanpa project references, setiap kali Anda mengubah core-library, TypeScript mungkin perlu memeriksa ulang ui-components dan web-app secara penuh. Dengan project references, TypeScript dapat memahami hubungan ini dan hanya melakukan type checking pada bagian yang relevan.

Bagaimana Cara Kerjanya?

Project references bekerja dengan beberapa cara:

  1. Incremental Builds: Hanya bagian proyek yang berubah yang akan di-build ulang. Ini menghemat waktu kompilasi secara drastis.
  2. Isolated Type Checking: Type checking untuk setiap proyek berjalan secara terpisah. Jika ada error di satu proyek, itu tidak selalu menghentikan type checking di proyek lain.
  3. Memahami Dependensi: TypeScript menggunakan file tsconfig.json di setiap proyek untuk membangun grafik dependensi.

Contoh Implementasi Sederhana di Monorepo

Mari kita lihat struktur monorepo contoh:

my-monorepo/
├── packages/
│   ├── core/
│   │   ├── src/
│   │   │   └── index.ts
│   │   └── tsconfig.json
│   ├── ui/
│   │   ├── src/
│   │   │   └── Button.tsx
│   │   └── tsconfig.json
│   └── app/
│       ├── src/
│       │   └── main.ts
│       └── tsconfig.json
└── tsconfig.json (root)

packages/core/tsconfig.json: Ini adalah library dasar.

{
  "compilerOptions": {
    "composite": true, // WAJIB untuk project references
    "declaration": true, // Menghasilkan file .d.ts
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "target": "es2019"
  },
  "include": ["src"]
}

packages/ui/tsconfig.json: ui bergantung pada core.

{
  ""extends": "../../tsconfig.json", // Inherit dari root
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "jsx": "react-jsx" // Contoh untuk React component
  },
  "include": ["src"],
  "references": [
    { "path": "../core" } // Referensi ke paket core
  ]
}

extends di sini merujuk ke tsconfig.json di root monorepo, yang akan kita buat nanti.

packages/app/tsconfig.json: app bergantung pada ui.

{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src",
    "jsx": "react-jsx"
  },
  "include": ["src"],
  "references": [
    { "path": "../ui" } // Referensi ke paket ui
  ]
}

tsconfig.json (root): Ini adalah file konfigurasi dasar yang di-extend oleh semua paket. Ini juga berisi daftar semua proyek yang menjadi bagian dari monorepo.

{
  "compilerOptions": {
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "module": "esnext",
    "moduleResolution": "bundler",
    "target": "es2019",
    "lib": ["es2019", "dom"],
    "allowJs": true,
    "checkJs": false,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "allowSyntheticDefaultImports": true
  },
  "references": [
    { "path": "./packages/core" },
    { "path": "./packages/ui" },
    { "path": "./packages/app" }
  ]
}

Membangun dengan Project References

Setelah konfigurasi di atas, Anda bisa menggunakan tsc --build (atau tsc -b) dari root monorepo.

npx tsc -b

TypeScript akan secara otomatis mendeteksi urutan build berdasarkan dependensi dan hanya membangun ulang proyek yang terpengaruh oleh perubahan. Ini jauh lebih cepat!

🎯 Kapan menggunakan composite: true?

5. Mengintegrasikan Project References dengan Monorepo Tooling

Meski tsc -b sudah sangat powerful, di monorepo yang lebih kompleks, Anda mungkin ingin mengintegrasikannya dengan tooling seperti Turborepo atau Nx. Tooling ini dibangun di atas konsep project references dan menyediakan fitur tambahan seperti:

// package.json di root monorepo (contoh dengan Turborepo)
{
  "name": "my-monorepo",
  "private": true,
  "workspaces": ["packages/*"],
  "scripts": {
    "build": "turbo run build",
    "typecheck": "turbo run typecheck"
  }
}

Dengan turbo run build, Turborepo akan menjalankan script build (yang biasanya memanggil tsc -b) di setiap paket, tetapi dengan caching dan optimasi yang cerdas.

💡 Manfaat utama integrasi ini:

6. Tips dan Best Practices

Untuk memaksimalkan manfaat Module Resolution dan Project References:

⚠️ Peringatan: Implementasi project references di monorepo bisa jadi sedikit rumit di awal, terutama jika Anda memiliki pengaturan build yang kompleks. Namun, investasi waktu di awal akan sangat terbayar di kemudian hari.

Kesimpulan

Mengelola aplikasi TypeScript skala besar, terutama dalam arsitektur monorepo, bukanlah tugas yang mudah. Namun, dengan memanfaatkan kekuatan Module Resolution dan Project References, Anda dapat mengubah tantangan menjadi keunggulan kompetitif.

Dengan konfigurasi yang tepat, Anda akan mendapatkan:

Jadi, jika Anda sedang berjuang dengan performa TypeScript di proyek besar Anda, saatnya untuk menyelami dan mengimplementasikan project references. Ini adalah langkah krusial menuju codebase yang lebih sehat, lebih cepat, dan lebih menyenangkan untuk dikembangkan!

🔗 Baca Juga