Skip to content

TypeScript Project References — Complete Guide

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about TypeScript Project References. We cover key concepts, practical examples, and best practices to help you master this topic.

TypeScript project references let you split a large codebase into smaller projects that reference each other, enabling incremental builds, faster type checking, and clear dependency boundaries between packages.

What You'll Learn

  • Composite projects and their configuration
  • The references field in tsconfig
  • Build mode (tsc --build)
  • Monorepo project structure

Why It Matters

In large codebases, a single tsconfig.json becomes a bottleneck. Every file is checked together, which is slow. Project references let TypeScript check only changed files and their dependents, dramatically reducing compilation time in monorepos.

Real-World Use

The Doda Browser extension monorepo has three projects: shared (types and utilities), extension-core (browser API), and extension-ui (React components). Each project has its own tsconfig. Shared types are compiled first, then consumed by the other two, ensuring clean separation and fast builds.

Learning Path

flowchart LR
  A[tsconfig Deep Dive] --> B[Project References]
  B --> C[Module Resolution]
  B --> D[You Are Here]
  C --> E[Source Maps]
  E --> F[Linting]

Composite Projects

A composite project is marked with composite: true:

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

composite: true enables declaration files and supports project references. Incremental builds use .tsbuildinfo files to track what changed.

Referencing Projects

// packages/extension-core/tsconfig.json
{
  "compilerOptions": {
    "composite": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "references": [
    { "path": "../shared" }
  ],
  "include": ["src"]
}

Root tsconfig with References

// tsconfig.json (root of monorepo)
{
  "files": [],
  "references": [
    { "path": "packages/shared" },
    { "path": "packages/extension-core" },
    { "path": "packages/extension-ui" }
  ]
}

The root tsconfig has an empty files array — it exists only to orchestrate the project references.

Build Mode

Use tsc --build instead of tsc:

# Build all projects in dependency order
npx tsc --build

# Clean all build output
npx tsc --build --clean

# Force rebuild all
npx tsc --build --force

# Build with watch mode
npx tsc --build --watch

Build mode:

  • Builds projects in topological order
  • Skips projects that haven't changed (incremental)
  • Reports errors from referenced projects
  • Supports --clean and --force

Monorepo Structure

my-monorepo/
├── tsconfig.json              # Root — orchestrates references
├── packages/
│   ├── shared/
│   │   ├── src/
│   │   │   └── index.ts
│   │   ├── dist/
│   │   └── tsconfig.json      # composite: true
│   ├── extension-core/
│   │   ├── src/
│   │   │   └── index.ts       # imports from shared
│   │   ├── dist/
│   │   └── tsconfig.json      # references shared
│   └── extension-ui/
│       ├── src/
│       ├── dist/
│       └── tsconfig.json      # references core
└── package.json

Incremental Builds

Enable incremental: true to use .tsbuildinfo files:

{
  "compilerOptions": {
    "incremental": true,
    "tsBuildInfoFile": "./dist/.tsbuildinfo"
  }
}

The .tsbuildinfo file stores compilation metadata so TypeScript only re-checks changed files on subsequent builds.

Declaration Files and Project References

When project A references project B, A needs B's .d.ts files (not .ts source). This is why composite requires declaration: true.

// packages/shared/src/types.ts
export interface User {
  id: string;
  name: string;
}

// packages/extension-core/src/index.ts
import { User } from "shared";
// TypeScript reads the .d.ts from shared/dist, not the .ts source

Common Mistakes

1. Not Marking Referenced Projects as composite

Referenced projects must have composite: true. Otherwise TypeScript errors.

2. Circular References

Project A cannot reference project B if B also references A (directly or transitively).

3. Missing outDir

All composite projects need outDir specified. TypeScript writes .d.ts and .tsbuildinfo to this directory.

4. Not Running tsc --build

Using plain tsc on the root won't build referenced projects. Always use tsc --build.

5. Ignoring Declaration Files

If a referenced project's .d.ts files are stale, consumers see outdated types. Always rebuild after changes.

Practice Questions

  1. What does composite: true do? Enables project references and requires declaration files and outDir.

  2. How do you build all referenced projects? npx tsc --build at the root.

  3. What is a .tsbuildinfo file? Stores incremental build metadata so TypeScript skips unchanged files.

  4. Can referenced projects have their own references? Yes. Dependencies are resolved transitively.

Challenge: Set up a monorepo with three packages: types (interfaces), utils (helper functions using types), and app (main application using utils). Configure project references and use tsc --build to compile.

FAQ

Can I use project references with npm workspaces?

Yes. They complement each other well — npm workspaces for runtime dependency resolution, project references for type checking.

What is the difference between `tsc` and `tsc --build`?

tsc compiles files matching the tsconfig. tsc --build orchestrates building all referenced projects in order.

Can I use project references with --watch?

Yes: tsc --build --watch watches all referenced projects.

Do project references affect the output JavaScript?

No. Only type checking and declaration generation are affected. The runtime output is unchanged.

Can I reference a project from a different directory?

Yes. The path is relative to the referencing tsconfig's location.

Mini Project: Monorepo Setup

mkdir -p monorepo/packages/{shared/src,app/src}
// monorepo/tsconfig.json
{ "files": [], "references": [{ "path": "packages/shared" }, { "path": "packages/app" }] }
// monorepo/packages/shared/tsconfig.json
{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "target": "ES2022",
    "module": "ESNext"
  },
  "include": ["src"]
}
// monorepo/packages/shared/src/index.ts
export interface ScanResult {
  id: string;
  threats: string[];
  clean: boolean;
}
// monorepo/packages/app/tsconfig.json
{
  "compilerOptions": {
    "composite": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "target": "ES2022",
    "module": "ESNext"
  },
  "references": [{ "path": "../shared" }],
  "include": ["src"]
}
// monorepo/packages/app/src/index.ts
import type { ScanResult } from "shared";

function processScan(result: ScanResult): void {
  if (result.clean) {
    console.log(`Scan ${result.id}: Clean`);
  } else {
    console.log(`Scan ${result.id}: ${result.threats.length} threats`);
  }
}

Build: cd monorepo && npx tsc --build

What's Next

Now explore module resolution in depth:

Lesson Description
{{< ref "/programming-languages/typescript/31-tsconfig-deep-dive" >}} Review tsconfig
{{< ref "/programming-languages/typescript/33-module-resolution" >}} Classic vs node, paths, baseUrl
{{< ref "/programming-languages/typescript/34-source-maps-debugging" >}} Debugging with source maps

Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro