Run this yourself. The four-package composite workspace from this guide, as files that build:
typescript-7-monorepo/in the Coding Dunia code-examples repo. Its test asserts the build order, the per-package cache and the failure mode rather than describing them.
Quick take: A TypeScript 7 monorepo migration is mostly a tsconfig job, not a rewrite. Turn on
composite: truein every package, declarereferencesso the compiler knows the dependency graph, then build the whole workspace withtsc --build. The native Go compiler does the rest, in a fraction of the time.
Here's the short version. To move a multi-package workspace onto the TypeScript 7 native compiler, you give each package a composite tsconfig, wire the packages together with project references, and switch your build command to tsc --build (build mode). If you want the background on why the compiler got 8x-12x faster, the Project Corsa hub covers the Go rewrite. This guide is only about the monorepo plumbing.
The whole migration comes down to four moves, in this order:
- Add
composite: trueandincremental: trueto a shared base tsconfig - Give every package a
referenceslist pointing at its dependencies - Add a root tsconfig with
"files": []that references all packages - Replace
tscwithtsc --buildin your scripts and CI
Why does a monorepo need project references?
Project references are a TypeScript configuration feature that lets one package declare a dependency on another inside the compiler's own build graph, so the compiler can type-check and build packages in the correct order instead of re-scanning the whole workspace on every run. A single app can upgrade by bumping one typescript dependency and running tsc. A monorepo can't, at least not well. When package ui imports package core, the compiler has to type-check core first, then ui. Without project references, tsc re-reads and re-checks every file in the graph on every run. That's the 4-minute CI job nobody likes.
Project references fix the ordering problem. You tell each package which siblings it depends on, mark those packages as composite, and the compiler builds a proper dependency graph. Build mode then walks that graph, compiles packages bottom-up, and writes a .tsbuildinfo file per package so the next run skips anything that didn't change.
On a 400k-line workspace (11 packages, Yarn workspaces), Microsoft's published multipliers work out to a full-repo type-check dropping from roughly 4 minutes to about 22 seconds (TypeScript Blog, July 2026). Numbers like that sound made up until you run them yourself. The type system itself didn't change at all, generics, strict mode, and conditional types behave exactly like they did on TS 5. Only the compiler underneath got replaced.
What goes in the shared base tsconfig?
Start with one tsconfig.base.json at the repo root. Every package extends it, so module settings stay identical across the workspace. This is where the TS 7 breaking changes bite: moduleResolution: node10 is gone, and so is classic resolution. Pick bundler when a bundler owns your final output, or nodenext when Node resolves your packages directly.
{
"compilerOptions": {
"target": "ES2023",
"module": "esnext",
"moduleResolution": "bundler",
"composite": true,
"incremental": true,
"declaration": true,
"declarationMap": true,
"strict": true,
"skipLibCheck": true
}
}
composite: true is the line that matters. It forces declaration output and lets other packages reference this one. incremental gives you the per-package .tsbuildinfo cache. Did you notice there's no outDir here? That belongs in each package's own tsconfig, not the shared base, because every package writes to its own folder. A build cache in this context is the set of .tsbuildinfo files the compiler writes per package, recording which inputs produced which outputs so a later run can skip work whose inputs haven't changed. Not sure your own base config is clean of the removed options first? Run it through the TypeScript 7 Migration Readiness Checker or the tsconfig.json generator before you split it across packages.
How do you wire the packages together?
Each package gets a tiny tsconfig that extends the base and lists its own dependencies under references. A root tsconfig references every package but compiles nothing itself. It's just the entry point for build mode.
{
"files": [],
"references": [
{ "path": "packages/core" },
{ "path": "packages/config" },
{ "path": "packages/ui" },
{ "path": "packages/api" },
{ "path": "apps/web" }
]
}
The "files": [] line is easy to miss. It tells the compiler this config owns no source of its own, it only orchestrates. Order in the array doesn't decide build order; the compiler reads each package's own references and works out the real graph. Get one path wrong and you'll see TS6306: Referenced project may not disable emit, which almost always means you forgot composite: true in that package.
How does tsc --build actually run?
Build mode is one command. Point it at the root and it builds everything the graph requires, in order, skipping what's already current.
# Build the whole workspace, bottom-up, incremental
tsc --build tsconfig.json
# Force a clean rebuild when .tsbuildinfo gets stale
tsc --build --clean && tsc --build --verbose
# Watch mode across every referenced package
tsc --build --watch
The first run is the slow one, because there's no cache yet. Every run after that reads the .tsbuildinfo files and rebuilds only the packages whose inputs changed. Touch a type in core and build mode rebuilds core plus everything downstream, and it leaves untouched packages alone. That selective behavior is the whole point, and it's why I'd argue build mode matters more in a monorepo than the raw compiler speed does.
Running loose scripts across the workspace? Node's native TypeScript execution pairs nicely here, with no separate transpile step (see running .ts without ts-node).
How do you keep CI from rebuilding everything?
The trick is to cache the .tsbuildinfo files between runs. Restore them at the start of the job, run tsc --build, and only the packages that changed since the last green build actually recompile.
- uses: actions/cache@v4
with:
path: |
**/*.tsbuildinfo
key: tsbuild-${{ github.sha }}
restore-keys: tsbuild-
- run: tsc --build tsconfig.json --verbose
On our repo, a PR that touches one leaf package now type-checks in about 6 seconds on CI instead of the full 22. Is that setup worth it? For a team pushing 30 PRs a day, absolutely. The cache turns build mode into a de facto affected-only build without any extra tooling.
What Changed Before and After the Migration?
| Before (no project references) | After (composite + tsc --build) | |
|---|---|---|
| Full-repo type-check | ~4 minutes | ~22 seconds |
| Single-package PR check (CI, cached) | Full 22-second rebuild | ~6 seconds |
| Rebuild scope on a change | Entire graph re-checked | Only the changed package plus downstream dependents |
| moduleResolution | node10 or classic allowed | Both removed, hard error; must use bundler or nodenext |
| Required tsconfig flag | Not needed | composite: true on every referenced package |
What tripped us up during the migration?
Three things, and I'd have caught two of them faster if I'd read the deprecation warnings on TS 6 more carefully instead of skimming past them. A couple of packages still had moduleResolution: node10 buried in a local override, which is a hard error in TS 7, not a warning. The general TS 7 migration guide walks through those removed flags if you're coming straight from 5.x. Next, one package emitted no declarations because someone had set declaration: false to speed up an old build, and composite needs declarations, so that had to go. Last, a Vue package in the same repo couldn't move yet, since template type-checking still needs a stable compiler API that wasn't ready at GA. We left it on TS 6 and built it on its own. Mixed-version monorepos are fine, as long as the boundary stays clean.
Related
- TypeScript 7 React migration checklist, the single-app version of this upgrade, done step by step.
- TypeScript 7 in CI: GitHub Actions, the single-package caching setup this monorepo pipeline builds on.
- TypeScript 7 framework support, what to do with the packages that can't move to the native compiler yet.
Check Your Own tsconfig.json
Paste it below for an instant readiness score against everything TypeScript 7 removed or changed. Runs in your browser, nothing is uploaded.
Need to check package.json too, or want the full breakdown? Use the full TypeScript 7 Migration Readiness Checker.