In monorepos and large apps, references are how type-checking stays fast and dependency boundaries stay enforced.
A single huge tsconfig.json makes every check and editor session load everything. Project references split the code into smaller projects with explicit dependencies.
Each referenced project sets "composite": true, which requires declaration output and that all source files are matched by include/files. A dependent project lists it under "references": [{ "path": "../core" }]. Imports across the boundary are then type-checked against the referenced project's generated `.d.ts` files rather than its sources, which is much faster and enforces that projects only depend on what they declare.
Build with `tsc --build` (tsc -b). Build mode:
- builds referenced projects first, in dependency order;
- skips projects that are up to date, using
.tsbuildinfofiles (incremental state); - supports
--watch,--verbose(explains why each project is rebuilt),--cleanand--force.
A common layout is a solution tsconfig at the root with "files": [] and references to every package, so tsc -b at the root builds everything and editors discover each project.
Practical tips:
- Turn on
declarationMapso go-to-definition jumps to the source.tsinstead of the.d.ts. - Keep tests in their own project referencing the code under test, so production code can't import test helpers.
incremental: truegives single projects the.tsbuildinfospeedup without references.isolatedDeclarations(TS 5.5) requires explicit types on exports, so declaration files can be produced per file by fast tools in parallel, without type-checking first.
Bundler-based monorepos often use workspace packages plus references only for type checking (emitDeclarationOnly or noEmit where allowed); the principle — small projects, explicit edges, cached builds — is the same.
Example
// tsconfig.json (solution file at the repo root)
{
"files": [],
"references": [{ "path": "./packages/core" }, { "path": "./packages/api" }]
}
// packages/core/tsconfig.json
{
"compilerOptions": {
"composite": true, "declarationMap": true,
"rootDir": "src", "outDir": "dist", "strict": true
},
"include": ["src"]
}
// packages/api/tsconfig.json: depends on core
{
"compilerOptions": { "composite": true, "rootDir": "src", "outDir": "dist", "strict": true },
"include": ["src"],
"references": [{ "path": "../core" }]
}
// Build everything incrementally: npx tsc -b --verboseRemember
composite: true+referencessplit a codebase into projectstsc -bbuilds dependencies first and skips up-to-date projects- Referenced projects are seen through their
.d.tsoutput - Use
declarationMapso navigation lands in source files
Watch out for
- Running plain
tsc -pon a project with references and expecting dependencies to be built — that needstsc -b.