πAdvancedAdvanced Integration
Monorepos & TypeScript Project References
Structure large codebases with TypeScript project references for incremental builds, proper cross-package type checking, and monorepo tooling integration.
Overview
As TypeScript codebases grow, a single `tsconfig.json` becomes a bottleneck: every file must be checked and compiled together, build times grow linearly, and there is no clear boundary between packages.
**TypeScript project references** (introduced in TS 3.0) solve this by composing multiple `tsconfig.json` files into a dependency graph. Each sub-project compiles independently and caches its output. When you change a file in package A, only A and packages that reference A are recompiled.
In a monorepo (a single repository containing multiple packages), project references provide:
- **Incremental compilation** - only recompile what changed
- **Type-safe cross-package imports** - TypeScript checks across package boundaries
- **Build ordering** - TypeScript knows which packages to compile first
- **IDE performance** - editors navigate to source instead of compiled `.d.ts` files
Popular monorepo tools (Turborepo, Nx, Lerna) integrate with TypeScript project references. Understanding how they work helps you debug build issues and optimize CI times.
Key Points
- β’Each package gets its own `tsconfig.json` with `"composite": true`
- β’`"composite": true` requires explicit `rootDir`, emits `.d.ts` files, and enables `--build` caching
- β’`references: [{ path: "../other-package" }]` declares a dependency on another tsconfig
- β’`tsc --build` (or `tsc -b`) builds in dependency order; `--watch` for incremental watch mode
- β’`paths` in `tsconfig.json` maps package names to source - avoids needing to `npm link`
- β’Root `tsconfig.json` typically has no `include` - only `references` to all sub-projects
- β’Turborepo and Nx use project references to determine what to cache and rebuild
Examples
Basic monorepo tsconfig structure
// Monorepo structure:
// /
// βββ packages/
// β βββ core/
// β β βββ src/index.ts
// β β βββ tsconfig.json <-- composite sub-project
// β βββ ui/
// β β βββ src/index.ts
// β β βββ tsconfig.json <-- composite sub-project (depends on core)
// β βββ app/
// β βββ src/index.ts
// β βββ tsconfig.json <-- composite sub-project (depends on ui, core)
// βββ tsconfig.json <-- root (no src, just references)
// βββ package.json
// packages/core/tsconfig.json
{
"compilerOptions": {
"composite": true, // Enables --build mode caching
"rootDir": "./src",
"outDir": "./dist",
"declaration": true, // Required by composite
"declarationMap": true, // Jump-to-source works in editors
"strict": true
},
"include": ["src/**/*"]
}
// packages/ui/tsconfig.json
{
"compilerOptions": {
"composite": true,
"rootDir": "./src",
"outDir": "./dist",
"declaration": true,
"declarationMap": true,
"strict": true
},
"references": [
{ "path": "../core" } // Depends on core
],
"include": ["src/**/*"]
}
// Root tsconfig.json - the build entry point
{
"files": [], // No files of its own
"references": [
{ "path": "./packages/core" },
{ "path": "./packages/ui" },
{ "path": "./packages/app" }
]
}Path mapping for cross-package imports without building first
// packages/ui/tsconfig.json - with path aliases
{
"compilerOptions": {
"composite": true,
"rootDir": "./src",
"outDir": "./dist",
"declaration": true,
"strict": true,
"baseUrl": ".",
"paths": {
"@myapp/core": ["../core/src/index.ts"] // Map to source directly
}
},
"references": [
{ "path": "../core" }
]
}
// packages/ui/src/Button.ts
import { formatText } from '@myapp/core'; // Works! Resolves to core/src/index.ts
// In editor: Go-to-definition jumps to source, not .d.ts
// During build: TypeScript uses the compiled output from core/dist
export function Button(label: string): string {
return `<button>${formatText(label)}</button>`;
}
// packages/core/src/index.ts
export function formatText(text: string): string {
return text.trim().toLowerCase();
}
// Build command at root
// tsc --build Build all packages in dependency order
// tsc --build --watch Watch mode - incremental on file change
// tsc --build --clean Delete all compiled outputTurborepo integration with TypeScript references
// turbo.json - define the build pipeline
{
"pipeline": {
"build": {
"dependsOn": ["^build"], // Build dependencies first
"outputs": ["dist/**"], // Cache these files
"inputs": ["src/**/*.ts", "tsconfig.json"]
},
"type-check": {
"dependsOn": ["^type-check"],
"outputs": [] // No output - pure check
}
}
}
// packages/core/package.json
{
"name": "@myapp/core",
"scripts": {
"build": "tsc --build",
"type-check": "tsc --noEmit"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
}
}
// packages/ui/package.json
{
"name": "@myapp/ui",
"dependencies": {
"@myapp/core": "*" // Local workspace reference
},
"scripts": {
"build": "tsc --build",
"type-check": "tsc --noEmit"
}
}
// Root package.json (npm/yarn/pnpm workspaces)
{
"name": "myapp-monorepo",
"private": true,
"workspaces": ["packages/*"],
"scripts": {
"build": "turbo run build",
"type-check": "tsc --build --noEmit"
}
}
// CI: only re-runs what changed since last commit
// npx turbo run build --filter=[HEAD^1] (changed packages only)Common Mistakes
- Forgetting `"composite": true` in sub-project tsconfigs: without it, `tsc --build` cannot cache outputs and will not understand the reference graph
- Importing from a package's `src/` directory directly in `package.json` dependencies instead of using path aliases: this bypasses the type-checker's dependency tracking and causes stale type information
- Not setting `declarationMap: true`: without declaration maps, Go-to-definition in editors jumps to the compiled `.d.ts` file instead of the TypeScript source, making debugging much harder
- Circular references between packages: TypeScript project references do not allow cycles. If package A references B and B references A, the build will fail with a circular dependency error
Note
Start with `tsc --build --verbose` to see exactly what TypeScript is and is not recompiling. The `--force` flag skips the cache (useful when debugging stale type errors). For large monorepos, storing the `.tsbuildinfo` cache files in CI is the single highest-impact optimization for build time.
Exercises
Write TypeScript code to solve each exercise. Click Run to compile and execute it right here, then mark as done.
Loading editorβ¦
Completed: 0 / 2 exercises
Knowledge Check
Question 1 of 4