This article is part of the TypeScript Project References series:
- Everything You Need to Know About TypeScript Project References
- Managing TypeScript Packages in Monorepos
- A new Nx Experience For TypeScript Monorepos and Beyond
Consider the following workspace:
1.
2├─ is-even
3│ ├─ index.ts
4│ └─ tsconfig.json
5├─ is-odd
6│ ├─ index.ts
7│ └─ tsconfig.json
8└─ tsconfig.json
9
And here we can see the relevant code:
1export function isEven(n: number): boolean {
2 return n % 2 === 0;
3}
4
1import { isEven } from 'is-even';
2
3export function isOdd(n: number): boolean {
4 return !isEven(n);
5}
6
If we try to run our build script on the is-odd
project, we see that TypeScript is unable to run the tsc
command because at the TypeScript level, is-odd
is not aware of the is-even
module:
~/is-odd❯
tsc
1index.ts:1:24 - error TS2792: Cannot find module 'is-even'. Did you mean to
2set the 'moduleResolution' option to 'nodenext', or to add aliases to the
3'paths' option?
4
51 import { isEven } from 'is-even';
6 ~~~~~~~~~
7
8
9Found 1 error in index.ts:1
10
11
TypeScript needs to be informed as to how to find the module named is-even
. The error message here actually suggests that we may have forgotten to add aliases to the paths
option. To do this we can adjust our tsconfig.json
file at the root of the monorepo:
1{
2 "compilerOptions": {
3 "paths": {
4 "is-even": ["./is-even/index.ts"],
5 "is-odd": ["./is-odd/index.ts"]
6 }
7 }
8}
9
By having the individual tsconfig.json
files extend
this base config, they will all get these paths, and now our build command will work.
~/is-odd❯
tsc
1
2
The biggest downsides with this approach is that it does not enforce any boundaries within your monorepo. At the TypeScript level we treat the entire monorepo as one "unit". The TypeScript path aliases we defined, while seeming to create boundaries, are really just a nicer alternative for relative imports.
To solve this, TypeScript introduced Project References. Let's have a look.
TypeScript Project References
By adding boundaries at the TypeScript level, we can significantly cut down on the "surface area" that TypeScript has to contend with when doing its job. This way, rather than TypeScript seeing our entire monorepo as one unit, it can now understand our workspace as a series of connected "islands" or nodes.
To add this to our previous example, we'll adjust the tsconfig.json
file for is-odd
since it depends on is-even
(note that the references
field is the only difference from the is-even/tsconfig.json
file):
1{
2 "extends": "../tsconfig.json",
3 "compilerOptions": {
4 "target": "esnext",
5 "module": "esnext",
6 "forceConsistentCasingInFileNames": true,
7 "strict": true,
8 "skipLibCheck": true
9 },
10 "references": [{ "path": "../is-even" }]
11}
12
Note that we still actually need path aliases for our example. This is because we still need a mechanism to resolve the location in the import statement:
1import { isEven } from 'is-even';
2
There are alternatives to path aliases to allow for this name to be resolved. The most recent enhancements in Nx use the workspaces
functionality of your package manager of choice (npm/pnpm/yarn/bun) as the way of resolving these names. With a few more adjustments to this set up, we can now use the -b
or --build
option when building is-odd
. One of these is turning on the composite
compiler option for each project, which we can do by setting the compilerOption
of composite
to true
at the root tsconfig.json
file - since our other tsconfig.json
files for the 2 different projects already extend our root file:
1{
2 "compilerOptions": {
3 "paths": {
4 "is-even": ["./is-even/index.ts"],
5 "is-odd": ["./is-odd/index.ts"]
6 },
7 "composite": true
8 }
9}
10
Let's run our build now with the --verbose
flag on:
~❯
tsc -b is-odd --verbose
1Projects in this build:
2 * is-even/tsconfig.json
3 * is-odd/tsconfig.json
4
5Project 'is-even/tsconfig.json' is out of date because buildinfo file
6'is-even/tsconfig.tsbuildinfo' indicates that file 'is-odd/index.ts' was root
7file of compilation but not any more.
8
9Building project
10'/Users/zackderose/monorepo-project-references/is-even/tsconfig.json'...
11
12Project 'is-odd/tsconfig.json' is out of date because buildinfo file
13'is-odd/tsconfig.tsbuildinfo' indicates that program needs to report errors.
14
15Building project
16'/Users/zackderose/monorepo-project-references/is-odd/tsconfig.json'...
17
Notice our filesystem now:
1.
2├─ is-even
3│ ├─ index.d.ts
4│ ├─ index.js
5│ ├─ index.ts
6│ ├─ tsconfig.json
7│ └─ tsconfig.tsbuildinfo
8├─ is-odd
9│ ├─ index.d.ts
10│ ├─ index.js
11│ ├─ index.ts
12│ ├─ tsconfig.json
13│ └─ tsconfig.tsbuildinfo
14└─ tsconfig.json
15
Notice how both is-even
AND is-odd
now have a compiled index.d.ts
declaration file and index.js
. They also both have a tsconfig.tsbuildinfo
file now (this holds the additional data TypeScript needs to determine which builds are needed). With the --build
option, TypeScript is now operating as a build orchestrator - by finding all referenced projects, determining if they are out-of-date, and then building them in the correct order.
Why This Matters
As a practical/pragmatic developer - the TLDR of all of this information is project references allow for more performant builds.
We've put together a repo to demonstrate the performance gains, summarized by this graphic:
In addition to the time savings we saw reduced memory usage (~< 1GB vs 3 GB). This makes sense given what we saw about how project references work. This is actually a very good thing for CI pipelines, as exceeding memory usage is a common issue we see with our clients for their TypeScript builds. Less memory usage means we can use smaller machines, which saves on the CI costs.
Can I use Project References in Nx?
Yes. You benefit from the performance gains of TypeScript project references the most in large monorepos. However, this is also when the biggest downsides of project references are felt, namely having to manually manage all the references in various tsconfig.json
files. This is what we help automate in Nx.
We're going to dive deeper into the new Nx experience with project references in one of the next articles of the series, but TL;DR, you can experiment with the setup now by either using the --preset=ts
:
❯
npx create-nx-workspace@latest foo --preset=ts
Or alternatively by appending the --workspaces
flag to other presets:
❯
npx create-nx-workspace@latest reactmono --preset=react --workspaces
Note, Angular doesn't work with TypeScript project references yet but we're looking into various options to make it happen.
Next up
Stay tuned for our next article in the series about managing TypeScript packages in monorepos.