Introduction
As TypeScript projects grow, compilation slows because the compiler must process every file. Project references split a large project into smaller sub-projects that can be compiled independently and incrementally. This dramatically reduces build times in monorepos and multi-package setups.
Key Concepts
- Project Reference: A tsconfig entry pointing to another sub-project, establishing a dependency relationship.
- composite: A tsconfig flag that marks a project as a referenceable sub-project. It requires
declaration: trueand enables incremental builds. - tsc --build (-b): A special build mode that compiles projects in dependency order, skipping up-to-date ones.
Real World Context
In a monorepo with libs/shared, apps/api, and apps/web, the API and web apps both depend on shared. Without project references, changing a file in shared requires recompiling everything. With references, tsc --build recompiles only shared and its downstream dependents.
Deep Dive
Setting Up Composite
Every referenced project must have composite: true:
json// libs/shared/tsconfig.json { "compilerOptions": { "composite": true, "declaration": true, "declarationMap": true, "outDir": "./dist", "rootDir": "./src" }, "include": ["src/**/*"] }
composite: true enforces three things:
declarationmust be true (generates.d.tsfiles)- All source files must be matched by
includeorfiles - Incremental build metadata is stored (
.tsbuildinfofiles)
Adding References
The consuming project references its dependencies:
json// apps/api/tsconfig.json { "compilerOptions": { "outDir": "./dist", "rootDir": "./src" }, "references": [ { "path": "../../libs/shared" } ], "include": ["src/**/*"] }
Root Build Configuration
A root tsconfig ties everything together:
json// tsconfig.json (root) { "files": [], "references": [ { "path": "./libs/shared" }, { "path": "./apps/api" }, { "path": "./apps/web" } ] }
Building
bashtsc --build # Build all projects in dependency order tsc --build --watch # Watch mode across all projects tsc --build --clean # Remove all build outputs
tsc --build is smart: it checks .tsbuildinfo files and only recompiles projects whose source files have changed.
Common Pitfalls
- Forgetting composite: true — Without it, the project cannot be referenced and
tsc --buildwill fail with a confusing error. - Importing source instead of declarations — Referenced projects should be imported by their package name or path alias, not by relative path to source files.
Best Practices
- Use --build for all monorepo compilation — Never use plain
tscon individual packages in a monorepo.tsc --buildrespects the dependency graph. - Enable declarationMap — It lets "Go to Definition" navigate to source files instead of
.d.tsfiles.
Summary
- Project references split large projects into independently compilable sub-projects.
composite: trueis required for any referenced project and enables incremental builds.tsc --buildcompiles in dependency order and skips up-to-date projects.- Root tsconfig with
"files": []and"references"ties the monorepo together.
Code Examples
// Root tsconfig.json for a monorepo
{
"files": [],
"references": [
{ "path": "./libs/shared" },
{ "path": "./libs/database" },
{ "path": "./apps/api" },
{ "path": "./apps/web" }
]
}
// libs/shared/tsconfig.json
{
"compilerOptions": {
"composite": true,
"declaration": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"]
}