If you have a TypeScript codebase in a monorepo that is not using TypeScript project references then you are missing a trick.
TypeScript project references have been around since TypeScript 3.0 and allow you to specify dependant packages in the tsconfig.json
that the current package depends on. When you build a package with dependencies, then the dependencies get built first.
The tsconfig.json
below specifies that this package references a common
package and that the common
package is built before the current package:
{ "extends": "../tsconfig-base.json", "compilerOptions": { "outDir": "../lib/animals", "rootDir": ".", }, "references": [ { "path": "../core" } ] }
Project references are specified via a references
array of objects with a path
property. The path
property is a relative path to a different location containing a tsconfig.json
file.
If the project were not using project references, then all packages would have to be built individually, which can be a huge drag if there are multiple dependent packages.
Building incrementally
The -b
or --build
switch is added to the tsc compiler when transpiling a package that has project references:
> tsc -b # Use the tsconfig.json in the current directory > tsc -b src # Use src/tsconfig.json > tsc -b foo/prd.tsconfig.json bar # Use foo/prd.tsconfig.json and bar/tsconfig.json
A successful build will output a tsconfig.tsbuildinfo
, used in subsequent builds to ensure only new code or changed code is built for a faster build time. The tsconfig.tsbuildinfo
file contains the signatures and timestamps of all files required to build the whole project. On subsequent builds, TypeScript will use that information to detect the least costly way to type check and emit changes to your project.
Watch all packages in the monorepo for changes
What has really boosted my TypeScript development is the ability to have tsc
watch for changes in every package of a monorepo.
I have a script like this running the whole time I am developing:
"watch": "tsc -b ./tsconfig.packages.json --watch"
When the watch
script is running, any TypeScript changes in the whole monorepo will result in a new efficient build:
Monorepo project structure
In most monorepos, the individual package nodes are in a folder named packages
. Below is an example monorepo with the various tsconfig.json
files strategically placed with surgical precision:
tsconfig.base.json # base tsconfig.json that all packages will extend tsconfig.packages.json # tsconfig.json that is used to watch eveything all packages /package.json /packages /common # Common utitliies used by all package /src .index.ts # entry point for package /tsconfig.json # Config file for 'common' project /package.json /web # Depends on 'common' /src .index.ts # entry point for package /tsconfig.json # Config file for 'web' project /api # Depends on 'common' /src .index.ts # entry point for package /tsconfig.json # Config file for 'api' project /package.json
At the root folder is a tsconfig.base.json
file that is extended by all packages in the monorepo:
{ "compilerOptions": { "baseUrl": ".", "composite": true, "declaration": true, "declarationMap": true, "paths": { "common": ["packages/common/src"], "web": ["packages/web/src"], "api": ["packages/api/src"] } } }
There is actually a lot going on here:
"composite": true
instructstsc
that there are project references"declarationMap": true
adds source maps for.d.ts
declaration files. It meansGo to Definition
in VS code and will go to the actual file and not the.d.ts
file- The
paths
field contains an object with each object key (common, web, or API in this example) pointing to a package’s location. With this set, we can import packages without a nasty relative or absolute path.
e.g.import { a } from 'web';
In this example, each package extends tsconfig.base.json
using the extends
option of tsconfig
:
{ "extends": "../tsconfig-base.json", "compilerOptions": { "rootDir": ".", }, "references": [ { "path": "../core" } ] }
Watching and building
The tsconfig.packages.json
at the root of the project structure references all packages that we want to watch or build:
{ "files": [], "references": [ { "path": "./packages/common" }, { "path": "./packages/web" }, { "path": "./packages/api" } ] }
With this structure in place, we can now build all packages in one swoop:
tsc -b ./tsconfig.packages.json
or watch:
tsc -b ./tsconfig.packages.json
Useful switches
The following switches are handy when working with project references:
--verbose: Prints out verbose logging to explain what is going on (may be combined with any other flag) --dry: Shows what would be done but does not actually build anything --clean: Deletes the outputs of the specified projects (may be combined with --dry) --force: Acts as if all projects are out of date --watch: Watch mode (may not be combined with any flag except --verbose)
Webpack
Bundlers like webpack and Rollup did not offer support for project references for quite some time, and Rollup only has partial support. This has not helped adoption.
webpack ts-loader
When configuring the webpack loader ts-loader
, you need to set the projectReferences
option to true:
{ test: /\.tsx?$/, use: { loader: 'ts-loader', options: { projectReferences: true, }, }, },
Epilogue
I do most of my main development in monorepo’s these days, both in client projects and my own personal development. If I use TypeScript as my main language, then I will enable project references just like I have outlined here. I find them a great productivity boost that not everyone is using.
LogRocket: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.
Try it for free.
Hey, Great article! It looks like at some point you may have changed the name of shared code from `core` to `common` and didn’t replace it throughout the examples. (Same with `tsconfig-base.json` vs. `ts-config.base.json`) Additionally, given the described folder structure, I believe the relative paths are incorrect for `extends`.
Thank you so much. I’ll see if I can get his updated