Understanding and Using TypeScript Project References
TypeScript Project References were introduced to solve a practical problem: large applications accumulate complexity quickly, and compiling everything from scratch becomes increasingly slow. Instead of working with a single massive tsconfig.json, Project References provide a predictable way to divide a project into smaller, independent units that can be built incrementally.
This article revisits the concept from the perspective of real project architecture. It explains the mechanics behind references, the reasoning for splitting configurations, and how to combine them with --build for production-grade performance. The goal is not to provide a checklist but to help understand how the feature fits into modern frontend and backend development workflows.
1. What Project References Actually Solve
In a typical codebase, different parts of the application evolve at different speeds. Some files rarely change, some change daily, and some are tightly coupled with specific environments such as frontend tooling or server infrastructure. Without references, the compiler treats everything as one unit even when only one part of the project was modified.
Project References address several long-standing needs:
Faster Builds Through Incrementality
TypeScript can skip entire parts of the project during compilation if their output is already up to date. This is especially valuable for monorepos and large libraries.
Clear Dependency Boundaries
When module A depends on module B, this relationship becomes explicit. TypeScript enforces the correct build order and prevents accidental circular dependencies.
Multiple tsconfig Files for Different Environments
Modern applications often combine browser code, server code, shared utilities, tests, scripts, and workers. Each part can have its own configuration tuned to its environment.
Repeatable and Predictable Builds
Project References make the compiler behave more like a traditional build system, offering deterministic output and verifiable build steps.
2. Defining References in tsconfig.json
At the heart of the system is the references field, which links one tsconfig.json file to another.
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}Each referenced configuration becomes a distinct compilation unit. TypeScript builds them in the correct order, caches the result, and reuses it when no files have changed.
What each file represents:
- tsconfig.app.json often handles client-side code compiled for browsers.
- tsconfig.node.json targets server-side execution, sometimes with different module systems or output directories.
This separation reduces friction when working with tools like Vite, Webpack, Bun, or Node’s native ESM loader.
3. Why Large Projects Benefit From Multiple Configurations
Different Module Systems
Frontend code typically uses ES Modules. Backend code may use CommonJS, ESM, or a hybrid format depending on deployment requirements.
Environment-Specific Features
For example:
- Browsers require DOM types.
- Node requires filesystem and process types.
- Shared TypeScript libraries require neither.
Having separate configurations allows each environment to rely only on the types and compiler settings it needs.
Isolation Improves Maintainability
When teams work on different parts of the same codebase, clear structure reduces confusion. Each package or module has a dedicated boundary defined by its configuration.
4. Example: Modular Project With References
Consider a project structured around independent modules:
/my-project
/module-a
tsconfig.json
index.ts
/module-b
tsconfig.json
index.ts
tsconfig.jsonRoot Configuration
{
"files": [],
"references": [
{ "path": "./module-a" },
{ "path": "./module-b" }
]
}The root does not compile anything directly. Its primary purpose is orchestration.
Module A Configuration
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "../dist/module-a",
"rootDir": "."
},
"include": ["index.ts"]
}Module B Configuration (Depends on A)
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"outDir": "../dist/module-b",
"rootDir": "."
},
"include": ["index.ts"],
"references": [{ "path": "../module-a" }]
}TypeScript now understands that Module B relies on Module A and ensures the correct build order.
5. Incremental Compilation With —build
The --build (or -b) flag activates TypeScript’s composite build mode. This mode:
- obeys the dependency graph
- compiles only what changed
- stores build metadata
- skips stale modules automatically
To compile the entire project:
tsc --buildTo inspect each step in the process:
tsc -b --verboseThis output is helpful when diagnosing slow builds or unexpected dependency behavior.
6. Composite Projects and Output Structure
To use references correctly, each referenced module must set:
"compilerOptions": {
"composite": true
}Composite mode enforces several rules:
- The project must have an explicit
outDir. - It must define
rootDirwhen needed. - Configuration must be predictable so that TypeScript can manage incremental builds.
These constraints ensure that modules follow a clear contract, making large-scale builds reliable.
7. Typical Use Cases in Modern Development
Frontend + Backend Monorepos
A shared utilities package can be referenced by both the frontend and the backend without duplicating logic or configurations.
Multi-Package Libraries
Libraries like React, Angular, or many internal enterprise packages separate parts of the system into loosely coupled modules.
Microservices and API Layers
Each service may contain its own domain models but still depend on shared contracts or schema definitions.
Developer Tooling Projects
CLI tools often combine runtime code with build-time utilities, and splitting them avoids unnecessary recompilation.
8. Debugging Common Reference Issues
Circular Dependencies
If two modules reference each other, TypeScript cannot determine build order. This often means the architectural boundary needs reconsideration.
Incorrect OutDir or RootDir
Misconfigured paths lead to files overwriting one another or appearing in unexpected directories.
Forgetting composite: true
Without composite mode, TypeScript ignores references entirely.
Mixing Module Formats
Building ES Module output that imports CommonJS output (or vice versa) can produce runtime errors. Each module should declare its environment explicitly.
Conclusion
Project References allow TypeScript to scale from small tools to complex multi-package architectures. They formalize boundaries, improve build times, allow environment-specific configuration, and produce predictable, incremental builds.
For teams working across multiple environments or maintaining large applications, adopting Project References is not simply a convenience—it is a necessary step toward sustainable long-term development.
With a clear structure, explicit dependencies, and incremental builds, TypeScript becomes capable of handling projects of any size without slowing development down.