Typescript Project Organization Monorepo Tsconfig Inheritance Shared Packages
Definisi
Project organization di TypeScript adalah cara memecah source code, config, build output, dan shared package supaya struktur repo tetap kebaca saat project tumbuh. Di tahap awal, satu app dengan satu tsconfig.json masih cukup. Tapi begitu ada beberapa aplikasi, shared package, test package, dan build target berbeda, organisasi project mulai menentukan apakah codebase masih enak dirawat atau justru jadi tumpang tindih.
Monorepo adalah satu repository yang menampung beberapa package atau app sekaligus. TypeScript sering dipakai di monorepo untuk memisahkan concern dengan jelas: aplikasi tetap fokus ke runtime, package shared fokus ke tipe, util, atau domain logic, dan konfigurasi diletakkan di level yang bisa diwariskan.
Kapan monorepo membantu
Monorepo membantu kalau beberapa hal ini sudah mulai terjadi:
- ada lebih dari satu aplikasi yang berbagi domain atau UI primitives
- shared types, utility, atau schema dipakai ulang lintas package
- release beberapa package perlu disinkronkan
- refactor besar harus konsisten di banyak package sekaligus
- kamu ingin satu source of truth untuk lint, test, tsconfig, dan tooling
Monorepo biasanya tidak terasa worth it kalau:
- cuma ada satu aplikasi kecil
- shared code masih sedikit dan jarang dipakai ulang
- tim belum butuh workflow build dan publish yang lebih kompleks
- overhead toolchain lebih mahal daripada manfaat reuse
Intinya: monorepo bukan tujuan, tapi alat untuk mengurangi duplikasi dan menjaga batas yang sehat antarbagian.
Struktur dasar project
Struktur yang umum dipakai:
repo/
├─ apps/
│ ├─ web/
│ └─ admin/
├─ packages/
│ ├─ ui/
│ ├─ types/
│ ├─ utils/
│ └─ config/
├─ tsconfig.base.json
├─ package.json
├─ pnpm-workspace.yaml / turbo.json / nx.json
└─ eslint.config.*Prinsipnya:
apps/berisi entry point yang dijalankan user.packages/berisi bagian yang reusable.tsconfig.base.jsonmenampung default yang dipakai bersama.- package masing-masing hanya override yang benar-benar spesifik.
Kalau struktur ini disiplin, kamu bisa baca repo dari atas ke bawah tanpa menebak-nebak package mana yang boleh mengimpor apa.
Cara kerja monorepo
Di monorepo, setiap package tetap punya batasnya sendiri. Import antarpackage sebaiknya lewat nama package, bukan lewat path relatif panjang yang menembus folder lain.
Contoh pola yang sehat:
apps/webmengimpor@acme/uiapps/webmengimpor@acme/typespackages/uitidak mengimpor file internalapps/admin
Kunci utamanya adalah dependency graph yang mengalir satu arah. Kalau package saling silang tanpa aturan, monorepo berubah jadi folder besar yang cuma kebetulan dibagi-bagi.
Diagram di atas menunjukkan pola dependency yang ideal:
- app memakai shared package
- shared package boleh bergantung pada package yang lebih dasar
- tidak ada dependency balik dari shared package ke app
tsconfig inheritance
TypeScript mendukung inheritance lewat extends. Ini dipakai untuk memisahkan config dasar dari config per-package.
Pola umum:
tsconfig.base.jsondi root berisi setting bersamapackages/*/tsconfig.jsonmewarisi base dan menambah override lokalapps/*/tsconfig.jsonmewarisi base tetapi bisa punya target, path, atau include yang beda
Contoh root base config:
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"noEmit": true,
"baseUrl": ".",
"paths": {
"@acme/ui": ["packages/ui/src/index.ts"],
"@acme/types": ["packages/types/src/index.ts"],
"@acme/utils": ["packages/utils/src/index.ts"]
}
}
}Contoh package-level config:
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "dist",
"composite": true,
"declaration": true,
"declarationMap": true
},
"include": ["src"]
}Prinsip inheritance yang perlu diingat:
extendsmewariskan opsi, bukan folder secara otomatis.include,exclude, danfilestetap perlu dipikirkan per package.- setting yang ditulis di child config akan menimpa config parent.
- path alias di root harus konsisten dengan struktur source, bukan output build.
Typing patterns untuk shared packages
Shared package biasanya dipakai untuk tiga jenis isi:
-
Types only package
- hanya export type, interface, enum, helper types
- tidak membawa runtime logic yang berat
-
Domain schema package
- menyimpan schema validasi, DTO, atau contract API
- type bisa di-generate atau di-infer dari schema
-
Shared utilities package
- helper kecil yang aman dipakai lintas app
- harus benar-benar generic, bukan business logic yang diam-diam ketergantungan ke app tertentu
Pola typing yang umum dan sehat:
export type UserId = string & { readonly __brand: "UserId" };
export interface User {
id: UserId;
name: string;
email: string;
}
export type ApiResponse<T> = {
data: T;
error: null | { message: string; code?: string };
};Kalau kamu punya shared contract, prioritaskan source of truth tunggal. Misalnya:
- schema validasi jadi sumber utama
- type diambil dari schema via inference
- app cukup konsumsi contract itu, bukan mendefinisikan ulang tipe serupa
Contoh pola yang sering berguna:
typealias untuk union, mapped type, atau utility result typeinterfaceuntuk object shape yang mungkin di-extend- branded type untuk membedakan ID yang bentuknya sama tapi maknanya beda
- generic wrapper untuk response, pagination, atau paginated resource
Jebakan umum saat share types across packages
1. Type sharing tanpa boundary runtime
Kalau package types mulai dipakai untuk runtime logic juga, package itu sering berubah jadi campuran aneh antara contract dan implementation. Akibatnya:
- dependensi jadi kabur
- tree-shaking bisa memburuk
- import type dan import runtime tercampur
Aturan aman: kalau package memang niatnya type-only, pertahankan sebagai type-only.
2. Circular dependency antarpackage
Contoh buruk:
apps/webimportpackages/uipackages/uiimportpackages/typespackages/typesimport util dariapps/web
Begitu dependency balik masuk, build dan editor mulai sulit memprediksi urutan resolve.
Solusi: pakai arah dependency yang tegas dan hindari shared package yang tergoda mengimpor application code.
3. Path alias yang cuma nyaman di editor
Path alias di tsconfig tidak otomatis bekerja di semua tool runtime.
Yang sering terjadi:
- TypeScript lolos
- IDE hijau
- runtime atau bundler gagal resolve alias
Karena itu alias harus diselaraskan dengan bundler, test runner, dan package export map.
4. Declaration file tidak sinkron
Kalau shared package menghasilkan .d.ts, tapi emit dan source-nya tidak konsisten, consumer bisa melihat type yang berbeda dari runtime nyata.
Biasanya muncul saat:
declarationaktif tapi source tidak dirapikan- path internal bocor ke consumer
- build output tidak sesuai
exportspackage
5. Terlalu banyak re-export
Pattern index.ts yang melempar semua export dari semua file memang nyaman, tapi mudah bikin boundary kabur.
Gunakan barrel export secukupnya:
- baik untuk public API
- buruk kalau dipakai untuk menyembunyikan struktur internal yang seharusnya eksplisit
6. Shared types yang terlalu spesifik ke satu app
Kalau packages/types berisi type yang sebenarnya hanya dipakai satu app, package itu akan membengkak tanpa manfaat nyata.
Tanda-tandanya:
- nama type penuh nama fitur spesifik app
- semua perubahan fitur memaksa update shared package
- package shared jadi tempat buang-buang tipe
Praktik konfigurasi yang rapi
Checklist sederhana:
- satu
tsconfig.base.jsondi root - child config hanya override yang perlu
- pakai path alias yang konsisten
- eksport public API dari satu entry point per package
- tandai boundary package dengan jelas di
package.json - kalau perlu build terpisah, aktifkan
compositedan referensi project
Contoh struktur referensi project:
{
"files": [],
"references": [
{ "path": "./packages/types" },
{ "path": "./packages/utils" },
{ "path": "./packages/ui" },
{ "path": "./apps/web" }
]
}Dengan pola ini, TypeScript bisa memahami urutan build dan dependency antarproject secara lebih stabil.
Kapan tidak perlu monorepo
Pilih repo biasa kalau:
- satu aplikasi belum butuh shared package
- tim masih kecil dan release cycle sederhana
- overhead tooling belum sebanding dengan reuse
- semua code masih lebih cepat dikelola dalam satu app
Kalau belum ada masalah struktur, jangan memaksa monorepo hanya karena terlihat lebih “scalable”. Struktur yang terlalu dini sering menambah beban tanpa menyelesaikan masalah nyata.
Ringkasan praktis
- Monorepo berguna saat beberapa package saling berbagi contract, utility, atau UI primitives.
tsconfig.base.jsonjadi satu sumber setting bersama.- Child config dipakai untuk override target, outDir, include, dan references.
- Shared types paling aman kalau punya boundary yang tegas dan tidak bercampur runtime logic.
- Jebakan terbesar biasanya circular dependency, alias yang tidak sinkron, dan package shared yang terlalu gemuk.
Referensi terkait
/dokumentasi/typescript/typescript-tsconfig-module-resolution-path-aliases-declaration-files-build-targets/dokumentasi/typescript/typescript-types-interfaces-generics-narrowing-utility-types/dokumentasi/typescript/typescript-advanced-types-unions-intersections-mapped-conditional-infer-type-guards