Project References and Monorepos

· 4 min read · Updated March 17, 2026 · advanced
typescript monorepo build scalability

As TypeScript projects grow, you eventually hit a wall: a single tsconfig.json becomes slow to compile, tests take forever to run, and managing dependencies feels chaotic. Project references and monorepos are the standard solution for scaling your TypeScript codebase while keeping build times manageable.

What Are Project References?

TypeScript’s project references let you split one big project into smaller, composable pieces. Instead of compiling everything with a single tsconfig.json, you create a graph of smaller projects that depend on each other.

Each sub-project has its own tsconfig.json with composite: true enabled. This tells TypeScript that the project can be referenced by others and that it will produce a .tsbuildinfo file for incremental builds.

// packages/shared/tsconfig.json
{
  "compilerOptions": {
    "composite": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"]
}

The key difference from a regular project is composite: true. This enables TypeScript’s build orchestration—when you build a project that depends on another, TypeScript automatically builds the dependencies first in the correct order.

Configuring References

To make one project reference another, add a references array to its tsconfig.json:

// packages/app/tsconfig.json
{
  "extends": "../../tsconfig.base.json",
  "compilerOptions": {
    "composite": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "references": [
    { "path": "../shared" },
    { "path": "../utils" }
  ]
}

When you run tsc —build on the app package, TypeScript:

  1. Checks which referenced projects need rebuilding
  2. Builds them in topological order
  3. Uses incremental build info to skip unchanged projects

This transforms a full rebuild from O(n) to O(1) for most changes—you only rebuild what’s actually modified.

The Monorepo Connection

Project references are the foundation of TypeScript monorepos. A monorepo is a single repository containing multiple packages or applications that share code. Several tools make this practical:

npm workspaces (Node.js native):

{
  "name": "my-monorepo",
  "workspaces": ["packages/*"]
}

pnpm workspaces (faster, strict):

# pnpm-workspace.yaml
packages:
  - 'packages/*'

Yarn workspaces:

{
  "workspaces": ["packages/*"]
}

These tools handle package installation and linking. Project references handle TypeScript compilation. Together, they give you a unified build system.

For larger projects, specialized tools add capabilities:

  • Nx: Caching, distribution, and advanced task orchestration
  • Turborepo: Remote caching and pipeline optimization
  • Lerna: Package publishing and changelog management

Incremental Builds and Build Info

The magic of project references is incremental compilation. When you build a composite project, TypeScript generates a .tsbuildinfo file tracking:

  • When each file was last compiled
  • Which files depend on which
  • The state of the output

On subsequent builds, TypeScript compares timestamps and only rebuilds what changed. This works both forward (when you modify a dependency) and backward (when you modify something that depends on a dependency).

# First build - full compilation
tsc --build ./packages/app

# Second build - only changed files
tsc --build ./packages/app
// File processing started...
// Processing 3 files...

The —build flag is required for project references to work. Running tsc without it treats the project as isolated.

Best Practices

Keep references shallow. A deep dependency graph increases build orchestration overhead. Favor a flat structure where possible.

Use a base config. Create a shared tsconfig.base.json that all projects extend:

// tsconfig.base.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "strict": true,
    "declaration": true,
    "declarationMap": true,
    "skipLibCheck": true
  }
}

Output to a separate directory. Never mix source and output in the same directory when using project references. Use outDir to keep them separate.

Be careful with rootDir. When projects reference each other, rootDir must be set correctly or you’ll get path resolution errors. Each project should have a clear root.

Common Pitfalls

Forgetting composite: true. Without it, references won’t work and TypeScript won’t produce the build info needed for incremental compilation.

Circular references. TypeScript will error if you create circular dependencies between projects. Break the cycle by extracting shared code into a new project.

Mismatched TypeScript versions. All projects in a monorepo should use the same TypeScript version to avoid inconsistencies in types and compilation behavior.

OutDir conflicts. If two projects output to the same directory, you’ll get conflicts. Ensure each project has a unique output directory.

When to Use Project References

Not every project needs project references. Consider them when:

  • Build times exceed 30 seconds for incremental changes
  • You have multiple packages sharing code
  • You want independent test and build pipelines
  • CI/CD pipelines rebuild too much on each change

For small projects, a single tsconfig.json is simpler and sufficient. Project references add orchestration complexity—only adopt them when the scale justifies it.

See Also