Blog
Zack DeRose
January 27, 2025

Everything You Need to Know About TypeScript Project References

TypeScript Project References Series

This article is part of the TypeScript Project References series:

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:

is-even/index.ts
1export function isEven(n: number): boolean { 2 return n % 2 === 0; 3} 4
is-even/index.ts
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:

Typescript cannot find 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:

tsconfig.json
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.

Successfully building 'is-odd' package

~/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.

Islands of TypeScript

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):

is-odd/tsconfig.json
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:

tsconfig.json
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:

Successful build with Typescript's 'Build Mode'

~

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:

results of the perf measurements for TypeScript project references

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.