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.
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.
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:
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
instructs tsc
that there are project references"declarationMap": true
adds source maps for .d.ts
declaration files. It means Go to Definition
in VS code and will go to the actual file and not the .d.ts
filepaths
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.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" } ] }
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
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)
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.
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, }, }, },
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 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.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowDing! You got a notification, but does it cause a little bump of dopamine or a slow drag of cortisol? […]
A guide for using JWT authentication to prevent basic security issues while understanding the shortcomings of JWTs.
Auth.js makes adding authentication to web apps easier and more secure. Let’s discuss why you should use it in your projects.
Compare Auth.js and Lucia Auth for Next.js authentication, exploring their features, session management differences, and design paradigms.
2 Replies to "Boost your productivity with TypeScript project references"
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