A TypeScript project often starts with a few source files and a simple configuration. As the application grows, developers add shared utilities, frontend components, API clients, and business logic. Eventually, the repository may contain several packages that depend on one another.
At that point, a small code change can trigger a large compilation workload. Developers may wait for the entire project to rebuild even when they have changed only one package.
TypeScript project references help organize large codebases into smaller, connected projects. With the right configuration, teams can build packages in dependency order and avoid rebuilding projects that do not need to change.
This approach is particularly useful for monorepos that contain multiple applications and shared TypeScript libraries.
In this article, you will learn how project references work, how to configure them, how to build a sample monorepo, and which mistakes can reduce their benefits.
What Are TypeScript Project References?
TypeScript project references allow one TypeScript project to depend on another through its tsconfig.json configuration.
Instead of treating the repository as one large compilation unit, you can divide it into smaller projects.
For example, consider a monorepo with three packages:
my-monorepo/
├── apps/
│ └── web/
│ ├── src/
│ └── tsconfig.json
├── packages/
│ ├── shared/
│ │ ├── src/
│ │ └── tsconfig.json
│ └── api-types/
│ ├── src/
│ └── tsconfig.json
├── tsconfig.json
└── package.jsonIn this structure:
api-typescontains shared TypeScript types.sharedcontains reusable utilities.webcontains the application.- The web application depends on the shared packages.
Without project references, developers might configure the application to compile source files from every package together. That can make builds harder to manage as the repository grows.
With project references, each package becomes a separate TypeScript project. The compiler can understand the dependency relationships and build the projects in the required order.
Project references do not automatically make every TypeScript operation faster. Their main benefit comes from establishing clear project boundaries and enabling build-mode compilation with incremental outputs.
Why Large Monorepos Need Project References
A monorepo can contain many independently maintained packages, but those packages are not necessarily independent during compilation.
Suppose an application imports a shared utility:
import { formatCurrency } from "@company/shared";If the shared package contains hundreds of files and the repository has several other packages, compiling everything as one project may create unnecessary work.
Project references provide several benefits.
Smaller Compilation Units
Each package can have its own compiler settings, source files, and output directory.
This makes it easier to identify which parts of the repository need to be built.
Dependency-Aware Builds
TypeScript can build referenced projects in dependency order when you use build mode.
If the application depends on a shared package, the referenced package is built before the application when necessary.
Incremental Compilation
Build mode uses project build information to determine whether projects need to be rebuilt. With suitable configuration and outputs, unchanged projects can often be skipped.
Clearer Package Boundaries
Project references encourage teams to define explicit dependencies instead of allowing packages to reach into one another's source directories.
This improves maintainability, although project references alone do not enforce every architectural rule a team may need.
Step 1: Configure the Root TypeScript Project
The root configuration coordinates the projects in the repository.
Create a root tsconfig.json:
{
"files": [],
"references": [
{
"path": "./packages/api-types"
},
{
"path": "./packages/shared"
},
{
"path": "./apps/web"
}
]
}The empty files array means the root configuration does not compile source files directly. Instead, it lists the projects that belong to the build.
The reference paths point to directories containing their respective tsconfig.json files.
The order of entries is not a substitute for defining the actual dependency relationships between projects. TypeScript uses the references declared in each project's configuration to understand those relationships.
You can use the root project to coordinate builds across the monorepo.
Step 2: Configure the Shared Types Package
Create packages/api-types/tsconfig.json:
{
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true,
"rootDir": "./src",
"outDir": "./dist",
"strict": true,
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler"
},
"include": ["src"]
}The important option is composite.
A project referenced by another TypeScript project must satisfy the compiler's requirements for composite projects. Enabling composite also changes certain defaults and checks, including requirements around source-file inclusion.
The other options control declaration output, source mapping, directory layout, type checking, and module resolution.
For example, create packages/api-types/src/index.ts:
export interface Product {
id: string;
name: string;
price: number;
}With declaration generation enabled, TypeScript can emit declaration files that describe the package's public types.
The consuming project can use these declarations as part of the project-reference workflow.
Step 3: Configure the Shared Utilities Package
The utilities package depends on the shared types package.
Create packages/shared/tsconfig.json:
{
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true,
"rootDir": "./src",
"outDir": "./dist",
"strict": true,
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler"
},
"references": [
{
"path": "../api-types"
}
],
"include": ["src"]
}The reference tells TypeScript that the shared package depends on the API types project.
Now create packages/shared/src/index.ts:
import type { Product } from "../../api-types/src";
export function formatProduct(product: Product): string {
return `${product.name} - $${product.price.toFixed(2)}`;
}This demonstrates the relationship, but production packages should generally expose a deliberate public module interface rather than depending on another package's source path.
For a package-based monorepo, configure package names and exports so consumers can import the published or built package interface. This avoids coupling consumers to internal source locations and makes local development more closely match production usage.
The build configuration must also align with the repository's module system and package resolution strategy.
Step 4: Configure the Application Project
The web application depends on both shared packages.
Create apps/web/tsconfig.json:
{
"compilerOptions": {
"composite": true,
"declaration": true,
"rootDir": "./src",
"outDir": "./dist",
"strict": true,
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler"
},
"references": [
{
"path": "../../packages/api-types"
},
{
"path": "../../packages/shared"
}
],
"include": ["src"]
}The application can now consume the referenced projects through their intended public interfaces.
Join the conversation! Your thoughts help the community grow.