references (project references)
Lists the projects this one depends on so tsc --build can order and cache the builds.
"references": { "path": string }[] // top-level, not inside compilerOptions- Added in TypeScript 3.0
Parameters
| Parameter | Type | Description |
|---|---|---|
$path | string | Path to a directory containing a tsconfig.json, or directly to a config file of any name. The referenced project must have composite enabled. |
Return value
Array<{ path: string }>A dependency edge to each listed project, so imports resolve to that project's built .d.ts and tsc --build compiles it first.
Errors
Error TS6306 if a referenced project is not composite. tsc -b behaves as though noEmitOnError were set, so a project with errors is not emitted and its dependents are skipped.
Changelog
- PHP 3.0Project references and tsc --build introduced.
- PHP 3.7disableSourceOfProjectReferenceRedirect added, letting very large solutions have the editor read built .d.ts instead of generating declarations in memory.
references is a top-level key, not a compilerOptions entry. Each { "path": "..." } declares that this project depends on another one. Two things follow: imports that resolve into the referenced project read its emitted .d.ts rather than its source, and tsc --build knows to compile that project first.
Why split at all
A single tsconfig.json over a whole repo re-checks everything on every change and lets any file import any other — nothing stops src importing test. Splitting into referenced projects gives each one its own build cache, so touching a leaf package does not re-check the world, and makes the dependency direction a compiler-enforced rule rather than a convention.
How to run it
tsc -b walks the graph, skips projects that are up to date, and builds the rest in dependency order. --verbose explains each decision, --dry shows the plan, --clean deletes outputs, --force rebuilds everything, --watch keeps it live. Plain tsc will not build dependencies — that is the most common source of confusion.
What trips people up
Since dependents consume .d.ts, a fresh clone has nothing to read until you run tsc -b once, and the editor shows errors everywhere until then. Add declarationMap so navigation still reaches source.
Example
// tsconfig.json (solution): { "files": [], "references": [
// { "path": "./core" }, { "path": "./api" }] }
// api/tsconfig.json: { "compilerOptions": { "composite": true },
// "references": [{ "path": "../core" }] }
// core/src/index.ts
export interface Invoice { id: string; cents: number }
export const total = (xs: Invoice[]): number =>
xs.reduce((sum, x) => sum + x.cents, 0);
// api/src/route.ts — this import resolves to core/dist/index.d.ts,
// which `tsc -b` builds before api is compiled.
const invoices: Invoice[] = [{ id: "a", cents: 500 }, { id: "b", cents: 250 }];
console.log(total(invoices)); // 750When to use it
- Splitting a monorepo so touching one package does not re-type-check every other one.
- Making it a compile error for src to import from test.
- Building packages in dependency order with a single tsc -b at the repo root.
- Giving each package its own strictness settings while still type-checking across boundaries.
Frequently asked questions
Why does tsc ignore my project references?
Because plain tsc does not build dependencies — you have to run tsc -b (or tsc --build). Without build mode the compiler will not compile referenced projects, so their .d.ts output may be missing or stale and imports across the boundary fail. Also check that references is a top-level key in tsconfig.json rather than nested inside compilerOptions, where it is silently ignored.
Do I need composite on every project in a monorepo?
On every project that appears in some other project's references array, yes — that is enforced, and TS6306 is the error when it is missing. A top-level solution file that only lists references and sets "files": [] does not need it, because nothing references the solution itself.
Why does my editor show errors right after cloning the repo?
Because referencing projects read the .d.ts output of their dependencies, and a fresh clone has none until something has been built. Run tsc -b once after cloning. Enabling declarationMap in each project also makes Go to Definition land on the original source rather than the generated declaration once the build exists.
More examples
Solution config plus two projects
"files": [] marks the root as a solution file — it compiles nothing itself and exists only to name the members. Running tsc -b at the root builds core, then api, and skips either one whose outputs are already newer than its inputs. api can import from core; core cannot import from api, because there is no edge in that direction.
// ./tsconfig.json — builds everything, contains no files of its own
{
"files": [],
"references": [{ "path": "./core" }, { "path": "./api" }]
}
// ./core/tsconfig.json
{
"compilerOptions": {
"composite": true, "declarationMap": true,
"outDir": "dist", "rootDir": "src"
},
"include": ["src/**/*"]
}
// ./api/tsconfig.json
{
"compilerOptions": {
"composite": true, "declarationMap": true,
"outDir": "dist", "rootDir": "src"
},
"include": ["src/**/*"],
"references": [{ "path": "../core" }]
}Driving the build
--verbose is the first thing to reach for when a change appears not to take effect; it names each project and states whether it was up to date. --force matters after operations that rewrite file timestamps, such as some checkout or restore-from-cache steps, because the up-to-date check compares input and output times and can be fooled into skipping real work.
tsc -b # build the graph rooted at ./tsconfig.json
tsc -b --verbose # explain why each project was or was not rebuilt
tsc -b --dry # print the plan without building
tsc -b --clean # delete every project's outputs
tsc -b --force # rebuild all, ignoring up-to-date checks
tsc -b --watch # rebuild affected projects as files change
# After a git checkout that did not preserve timestamps:
tsc -b --force
Discussion