|$ curl https://forge-ai.dev/api/markdown?path=docs/typescript/project-refs
$cat docs/typescript-—-project-references.md
updated Recently·10 min read·published

TypeScript — Project References

TypeScriptAdvanced
Introduction

Project references (TypeScript 3.0+) allow you to split a large codebase into smaller TypeScript projects that reference each other. This enables incremental compilation — only changed projects are rebuilt — dramatically improving build times in monorepos and large codebases.

Composite Projects

A composite project is a TypeScript project that can be referenced by other projects. It requires specific settings: composite: true, declaration: true, and declarationMap: true (recommended).

packages/shared/tsconfig.json
JSON
1{
2 "compilerOptions": {
3 "composite": true,
4 "declaration": true,
5 "declarationMap": true,
6 "outDir": "./dist",
7 "rootDir": "./src",
8 "target": "ES2022",
9 "module": "ESNext",
10 "moduleResolution": "bundler",
11 "strict": true
12 },
13 "include": ["src"]
14}

The composite flag tells TypeScript that this project is a unit of compilation. It enforces that all input files are included in the include or files array, and that declaration is enabled.

packages/shared/src/utils.ts
TypeScript
1// packages/shared/src/utils.ts
2export function formatDate(date: Date): string {
3 return date.toISOString().split("T")[0];
4}
5
6export function clamp(value: number, min: number, max: number): number {
7 return Math.min(Math.max(value, min), max);
8}
9
10export type Result<T, E = Error> =
11 | { ok: true; value: T }
12 | { ok: false; error: E };
13
14export function tryCatch<T>(fn: () => T): Result<T> {
15 try {
16 return { ok: true, value: fn() };
17 } catch (e) {
18 return { ok: false, error: e as Error };
19 }
20}
Setting Up References

A project declares its dependencies using the references array in tsconfig.json. Each reference points to the directory of another composite project.

packages/api/tsconfig.json
JSON
1{
2 "compilerOptions": {
3 "composite": true,
4 "declaration": true,
5 "declarationMap": true,
6 "outDir": "./dist",
7 "rootDir": "./src",
8 "target": "ES2022",
9 "module": "ESNext",
10 "moduleResolution": "bundler",
11 "strict": true,
12 "paths": {
13 "@shared/*": ["../shared/src/*"]
14 }
15 },
16 "references": [
17 { "path": "../shared" }
18 ],
19 "include": ["src"]
20}
packages/api/src/server.ts
TypeScript
1// packages/api/src/server.ts
2import { formatDate, clamp } from "@shared/utils";
3import type { Result } from "@shared/utils";
4
5interface User {
6 id: number;
7 name: string;
8 createdAt: Date;
9}
10
11function getUser(id: number): Result<User> {
12 if (id <= 0) {
13 return { ok: false, error: new Error("Invalid ID") };
14 }
15 return {
16 ok: true,
17 value: {
18 id,
19 name: "Alice",
20 createdAt: new Date(),
21 },
22 };
23}
24
25function formatUser(user: User): string {
26 return `${user.name} (joined ${formatDate(user.createdAt)})`;
27}
28
29const result = getUser(1);
30if (result.ok) {
31 console.log(formatUser(result.value));
32}

best practice

Place references in the consuming project, not the dependency. The pathshould point to the directory containing the dependency's tsconfig.json, not the tsconfig file itself.
Build Mode (--build)

The --build flag (tsc -b) enables project reference awareness. It builds each project in dependency order, skips up-to-date projects, and produces declaration files for downstream consumption.

terminal
Bash
1# Build a project and all its dependencies
2tsc -b packages/api
3
4# Build everything — respects dependency graph
5tsc -b
6
7# Build with verbose output (see what's being built)
8tsc -b --verbose
9
10# Watch mode — rebuild on changes
11tsc -b --watch
12
13# Force rebuild (ignore cache)
14tsc -b --force
15
16# Clean build — delete all output directories
17tsc -b --clean
18
19# Build a specific project
20tsc -b packages/shared
21tsc -b packages/api
tsconfig.json (root)
JSON
1{
2 "references": [
3 { "path": "./packages/shared" },
4 { "path": "./packages/api" },
5 { "path": "./packages/web" }
6 ]
7}

The root tsconfig.json can be a solution file that lists all projects. Running tsc -b from the root builds everything in the correct order.

Incremental Builds

Project references combined with --buildmode enable incremental compilation. TypeScript tracks which projects have changed and only rebuilds what's necessary.

tsconfig.incremental.json
JSON
1{
2 "compilerOptions": {
3 "composite": true,
4 "declaration": true,
5 "incremental": true,
6 "tsBuildInfoFile": "./.tsbuildinfo",
7 "outDir": "./dist",
8 "rootDir": "./src",
9 "target": "ES2022",
10 "module": "ESNext",
11 "moduleResolution": "bundler"
12 }
13}

The tsBuildInfoFile stores compilation state. On subsequent builds, TypeScript checks timestamps and content hashes to skip unchanged files. For large monorepos, this can reduce build times from minutes to seconds.

terminal
Bash
1# First build — compiles everything
2tsc -b --verbose
3# [12:00:01] Projects in this build:
4# * packages/shared/tsconfig.json
5# * packages/api/tsconfig.json
6# * packages/web/tsconfig.json
7# [12:00:01] Project 'packages/shared/tsconfig.json' is up to date
8# [12:00:01] Project 'packages/api/tsconfig.json' is up to date
9# [12:00:01] Project 'packages/web/tsconfig.json' is up to date
10
11# Change a file in shared — only shared and dependents rebuild
12tsc -b --verbose
13# [12:00:05] Project 'packages/shared/tsconfig.json' is out of date
14# [12:00:05] Building project 'packages/shared/tsconfig.json'
15# [12:00:06] Project 'packages/api/tsconfig.json' is out of date
16# [12:00:06] Building project 'packages/api/tsconfig.json'
17# [12:00:07] Project 'packages/web/tsconfig.json' is out of date
18# [12:00:07] Building project 'packages/web/tsconfig.json'
19
20# Unchanged build — everything is cached
21tsc -b --verbose
22# [12:00:10] All projects are up to date

info

For incremental builds to work correctly, each project must have composite: true and its own outDir. Declaration files (.d.ts) are the contract between projects — they allow downstream projects to type-check without rebuilding the dependency.
Monorepo Patterns

In a monorepo, project references create a dependency graph. Each package is a composite project, and the root tsconfig.json ties them together. This pattern scales from small teams to large organizations.

structure
Bash
1# Typical monorepo structure
2my-project/
3├── tsconfig.json # Solution file (references all projects)
4├── packages/
5│ ├── shared/
6│ │ ├── tsconfig.json # composite: true
7│ │ └── src/
8│ │ └── index.ts
9│ ├── api/
10│ │ ├── tsconfig.json # references: [shared]
11│ │ └── src/
12│ │ └── server.ts
13│ └── web/
14│ ├── tsconfig.json # references: [shared]
15│ └── src/
16│ └── app.tsx
packages/api/tsconfig.json
JSON
1{
2 "compilerOptions": {
3 "composite": true,
4 "declaration": true,
5 "outDir": "./dist",
6 "rootDir": "./src",
7 "target": "ES2022",
8 "module": "ESNext",
9 "moduleResolution": "bundler",
10 "strict": true,
11 "paths": {
12 "@my-project/shared": ["../shared/src"]
13 }
14 },
15 "references": [
16 { "path": "../shared" }
17 ],
18 "include": ["src"]
19}

best practice

Keep your dependency graph acyclic. If package A depends on B, B should never depend on A. Circular references break incremental builds and defeat the purpose of project references.
Output Structure

Each composite project outputs its compiled files and declaration files to its own outDir. Downstream projects consume the .d.ts files, not the source code.

output
Bash
1# After building with tsc -b
2packages/
3├── shared/
4│ └── dist/
5│ ├── index.js
6│ ├── index.d.ts # ← consumed by api and web
7│ ├── index.d.ts.map
8│ ├── utils.js
9│ ├── utils.d.ts # ← consumed by api and web
10│ ├── utils.d.ts.map
11│ └── .tsbuildinfo # ← build cache
12├── api/
13│ └── dist/
14│ ├── server.js
15│ ├── server.d.ts
16│ ├── server.d.ts.map
17│ └── .tsbuildinfo
18└── web/
19 └── dist/
20 ├── app.js
21 ├── app.d.ts
22 ├── app.d.ts.map
23 └── .tsbuildinfo
Best Practices & Common Pitfalls

Always use tsc -b for project references. Running plain tsc ignores project references and builds nothing. tsc -b is the only way to build with references.

Build order matters. tsc -b respects the dependency graph and builds projects in topological order. If you build manually, ensure dependencies are built first.

Use --watch for development. tsc -b --watch watches all projects and rebuilds affected ones on change. It's faster than running tsc -b repeatedly.

Keep outDir and rootDir consistent. Each project must have its own outDir. If two projects share the same outDir, their outputs will collide and cause confusing errors.

Never edit files in outDir. The output directory is managed by tsc. Manual changes are overwritten on the next build. If you need to customize output, use a build script after tsc.

Use composite with bundlers carefully. If you use esbuild or swc for production builds, keep composite + declaration for type-checking only. The bundler handles the actual output.

warning

A common pitfall: forgetting to set "references"in the consuming project. Without it, TypeScript won't know about the dependency and will fail to resolve imports from the other project's output.
$Blueprint — Engineering Documentation·Section ID: TS-PROJ·Revision: 1.0