Skip to Content
Course content

58: Project References for Monorepos

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Why can't I just use path aliases for my monorepo?

I see this a lot. You've got a shared folder and an app folder, and you've set up paths in your tsconfig.json so you can import @my-project/shared without using a dozen relative dots. It works for the IDE, but it's a lie to the compiler.

Path aliases are just "shortcuts" for the type checker. They don't establish a real dependency relationship. If you change a type in your shared library, TypeScript doesn't inherently know that the app needs to be re-checked or rebuilt. In a massive project, you end up running tsc on the whole world every time, which is a productivity killer. Project References turn those "shortcuts" into a formal graph. It tells TypeScript: "The app depends on the shared library; don't even bother looking at the app until the shared library is valid and built."

How do I actually configure these references without it breaking?

It boils down to two main things: the composite flag and the references array. Let's say we have a core package and a web-app package.

First, in your core/tsconfig.json, you must set composite: true. This tells TypeScript that this project is intended to be a dependency for other projects and enables the generation of .d.ts files automatically.

// core/tsconfig.json
{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "outDir": "./dist",
    "rootDir": "./src"
  }
}

Then, in your web-app/tsconfig.json, you point back to that directory. I usually suggest keeping your configs lean and using extends if you have a base config, but the reference is the critical part:

// web-app/tsconfig.json
{
  "compilerOptions": {
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "references": [
    { "path": "../core" }
  ]
}

One thing I've learned the hard way: make sure your outDir is set. Since composite projects rely on build artifacts (the .d.ts files) to speed things up, TypeScript needs a consistent place to find them.

Do I still have to run tsc in every single folder?

God, no. That would be a nightmare. Once you have project references set up, you stop using the standard tsc command and start using the build mode: tsc --build (or tsc -b for short).

I love tsc -b because it's smart. It looks at the timestamps of your files and the produced .d.ts files. If you only changed a file in web-app and didn't touch core, it'll skip rebuilding core entirely. It's essentially a build system baked directly into the compiler.

You can even create a "solution" tsconfig.json at the root of your monorepo that doesn't compile any code itself, but simply lists all your projects. It looks like this:

// tsconfig.json (Root)
{
  "files": [],
  "references": [
    { "path": "./core" },
    { "path": "./web-app" }
  ]
}

Now, you just run tsc -b from the root, and TypeScript handles the entire dependency tree for you. It's a massive quality-of-life upgrade once your project grows beyond a single directory.




📋 Practical Task

Refactoring the "Auth-Service" Dependency Graph

You have a monorepo with two directories: packages/auth-lib and packages/api-server. Currently, api-server imports types from auth-lib using relative paths, and the whole project is compiled using one giant tsconfig.json at the root, which is becoming incredibly slow.

Your goal: Convert this to use Project References.

  • Create a tsconfig.json inside packages/auth-lib that enables composite and specifies an outDir of ./dist.
  • Create a tsconfig.json inside packages/api-server that references the auth-lib directory.
  • Create a root-level tsconfig.json "solution" file that references both packages but compiles no files of its own.
  • Verify the setup by running the correct tsc command from the root to build the entire project incrementally.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.