Skip to Content
Course content

169: Project References for Large Codebases

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

Once your TypeScript project grows past a few dozen files, you'll start noticing that tsc is taking longer and longer to run. The compiler is essentially re-checking the entire world every time you change a single line of code. In a professional environment, this kills productivity. Project References are the solution to this; they allow us to break a monolithic codebase into smaller, independent pieces that can be compiled incrementally.

Structuring a core library and a client app

Let's imagine we're building a system where we have a shared core package (containing our business logic and types) and a client package (the actual application). I want the client to depend on core, but I don't want the compiler to re-analyze core every time I tweak a CSS-in-JS style in the client.

First, I'll set up my folder structure like this:


/my-project
  ├── core/
  │   ├── tsconfig.json
  │   └── index.ts
  └── client/
      ├── tsconfig.json
      └── main.ts

In core/index.ts, I'll put a simple data model:


export interface User {
    id: string;
    username: string;
}

export const formatUser = (user: User) => `User: ${user.username}`;

Configuring the dependency boundary

Now, here is where the magic happens. To make a project "referenceable," it needs a specific flag in its tsconfig.json. I'll set up the core config first:


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

The composite: true flag is the key here. It tells TypeScript that this project is intended to be referenced by others, which forces it to generate .d.ts files (declarations) so that the consuming project doesn't have to re-compile the source code of the library.

The "Missing Reference" mistake

Now I'll set up the client. I'll create client/main.ts to use that formatUser function:


import { User, formatUser } from "../core/index";

const me: User = { id: "1", username: "dev_pro" };
console.log(formatUser(me));

And the client/tsconfig.json:


{
    "compilerOptions": {
        "outDir": "../dist/client",
        "rootDir": ".",
    },
    "references": [
        { "path": "../core" }
    ]
}

I'll try to run tsc in the client folder. But wait—I get an error. TypeScript is complaining that it can't find the declaration files for core. I realized I made a classic mistake: I'm trying to compile the client, but I haven't actually built the core project yet. Project references don't magically compile dependencies on the fly during a standard tsc call; they rely on the dependency already being built into its outDir.

I could manually run tsc in the core folder first, but that's tedious. Instead, I'll use the build mode flag.

Using the Build Mode for incremental success

Instead of running tsc, I'll use tsc --build (or tsc -b) from the root of the project. This is the "smart" way to handle referenced projects.


# From the project root
tsc -b client

When I run this, TypeScript looks at the references array in the client config, sees that it depends on core, checks if core has changed, builds core if necessary, and then builds client. It's a dependency graph in action.

If I change a file in client and run tsc -b client again, you'll notice it's nearly instantaneous. TypeScript knows core hasn't changed, so it skips it entirely. This is how you keep a massive codebase from feeling like a slog.




📋 Practical Task

Exercise: Splitting a Shared Utility Library from a Web Application

You have a project with a shared utility folder and a main application folder. Your goal is to implement Project References to ensure the utility library is compiled independently.

Requirements:

  • Create a folder named utils with a tsconfig.json. Ensure it is configured as a composite project that outputs to ../dist/utils.
  • Create a file utils/math.ts that exports a function add(a: number, b: number): number.
  • Create a folder named app with a tsconfig.json. Configure it to reference the utils project.
  • Create a file app/index.ts that imports add from ../utils/math and logs the result of add(5, 10).
  • Verify your setup by running tsc -b app from the root directory and ensure the dist folder contains the compiled output for both projects.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.