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:

Code
my-monorepo/
├── apps/
│   └── web/
│       ├── src/
│       └── tsconfig.json
├── packages/
│   ├── shared/
│   │   ├── src/
│   │   └── tsconfig.json
│   └── api-types/
│       ├── src/
│       └── tsconfig.json
├── tsconfig.json
└── package.json

In this structure:

  • api-types contains shared TypeScript types.
  • shared contains reusable utilities.
  • web contains 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:

TypeScript
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:

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:

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:

TypeScript
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:

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:

TypeScript
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:

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.

For example, an application file might use a package export:

TypeScript
import type { Product } from "@company/api-types";
import { formatProduct } from "@company/shared";

const product: Product = {
    id: "P100",
    name: "Keyboard",
    price: 49.99
};

console.log(formatProduct(product));

The @company/api-types and @company/shared names are illustrative package names. To use this code, define the corresponding workspace packages and their package exports, or adapt the imports to your existing package configuration.

Project references do not automatically create npm package aliases. Package names, workspace configuration, build outputs, and module resolution must be configured separately.

Step 5: Build the Monorepo in Dependency Order

Use TypeScript build mode to build the root project:

Bash
npx tsc -b

Build mode reads the project references, determines the dependency order, and builds projects when needed.

For subsequent builds, TypeScript can use generated build information to skip projects that do not require rebuilding.

You can also force a rebuild:

Bash
npx tsc -b --force

This is useful when you need to verify a clean compilation across the referenced projects, but it removes much of the time-saving benefit of incremental builds.

To remove build outputs and build information, use:

Bash
npx tsc -b --clean

This cleans outputs associated with the configured build projects. Check your output directories and build configuration before running it, especially if generated files contain artifacts that other tools need.

For more detailed build diagnostics, use:

Bash
npx tsc -b --verbose

Verbose output helps explain which projects are being built and why.

Step 6: Understand Incremental Build Behavior

Project references become especially useful when a monorepo has stable dependencies and clear package boundaries.

Consider this example:

Code
api-types
    |
    v
shared
    |
    v
web

If a developer changes an implementation detail in web, TypeScript may be able to rebuild only the application project.

If a public type changes in api-types, dependent projects may need rebuilding because their type-checking assumptions can change.

The actual work depends on the compiler version, configuration, generated outputs, and the nature of the change.

Change

Likely build impact

Change a private implementation detail in the web app

The web project may be the only project that needs rebuilding

Change a public type in the API types package

Dependent projects may need rebuilding

Change a shared utility's implementation

The shared package and potentially its consumers may need rebuilding

Delete generated outputs

Affected projects may need to rebuild their outputs

Run a forced build

All configured projects are checked for rebuilding regardless of normal incremental decisions

Do not assume that every source change will produce a minimal build. Monitor actual build output and timings to understand how your repository behaves.

Step 7: Separate TypeScript Builds From Bundler Builds

A common source of confusion is assuming that TypeScript project references automatically configure every part of the frontend build.

They do not.

A frontend application may use Vite, Webpack, another bundler, or a framework-specific build system. That tool may handle JavaScript bundling separately from TypeScript's project build process.

For example:

JSON
{
  "scripts": {
    "typecheck": "tsc -b",
    "build": "vite build"
  }
}

This is an illustrative setup. It assumes the root TypeScript configuration represents the projects that should be type-checked and that Vite is configured separately to build the application.

Depending on the repository, you may need to run package builds before the bundler, use workspace scripts, or configure the bundler to consume source files directly.

For production, make sure your CI pipeline runs the required type-checking and bundling steps rather than assuming one command replaces the other.

Common Mistakes to Avoid

Mistake

Why it causes problems

How to fix it

Forgetting composite on referenced projects

The referenced project does not satisfy project-reference requirements

Enable composite and configure the required project options

Treating the root configuration as a normal source project

It may compile files unintentionally or duplicate work

Use an appropriate solution-style root configuration

Importing arbitrary files from another package's source directory

Creates unclear package boundaries and can conflict with output settings

Expose a public package interface

Assuming references create package aliases

Module resolution may still fail

Configure workspace packages, exports, and imports

Expecting every build to be faster

Large dependency changes may still require extensive work

Measure build times and inspect verbose output

Ignoring generated declaration files

Consumers may not see the expected type information

Check declaration output and package configuration

Using different build systems without coordination

Bundling and type checking can become inconsistent

Define explicit build and type-check scripts

Best Practices for Large TypeScript Repositories

  • Divide the repository along meaningful package boundaries.
  • Use composite for projects that participate in project references.
  • Generate declaration files for referenced library projects where appropriate.
  • Keep package imports aligned with public exports.
  • Use tsc -b to coordinate referenced project builds.
  • Keep output directories and generated files consistent across packages.
  • Avoid circular project dependencies.
  • Use verbose build output when diagnosing unexpected rebuilds.
  • Measure cold builds and incremental builds separately.
  • Keep TypeScript version and compiler configuration consistent across the repository.
  • Integrate type checking and application bundling deliberately in CI/CD.
  • Review whether project references actually improve your build time before adding unnecessary configuration.

Advantages and Limitations of Project References

Advantages

Limitations

Establish clear boundaries between TypeScript projects

Require more configuration than a single project

Support dependency-aware builds

Changes to shared public types may affect many projects

Enable incremental build workflows

Generated declarations and build outputs must be managed

Help organize large monorepos

Do not automatically configure package aliases or bundlers

Improve maintainability through explicit dependencies

Benefits depend on project structure and build behavior

Project references are most valuable when a repository contains well-defined packages with manageable dependencies. They are less useful when every package imports internal files from every other package or when the build pipeline ignores the TypeScript project structure.

Summary

TypeScript project references help organize large monorepos into smaller projects with explicit dependencies. Combined with build mode and incremental compilation, they can reduce unnecessary work and make large codebases easier to maintain.

Start by creating a root configuration, enabling composite on referenced projects, and defining the dependency relationships between packages. Then use tsc -b to coordinate builds and inspect verbose output to understand what gets rebuilt.

Remember that project references do not replace package configuration, bundling, or application testing. The best results come from clear package boundaries, consistent compiler settings, and a build pipeline that uses those boundaries effectively.