TypeScript Project References and Monorepos
As TypeScript projects grow, compiling them can become slow. Project References allow you to structure your TypeScript programs into smaller pieces (sub-projects). This improves build times, enforces logical separation between components, and organizes code better.
This feature is the backbone of TypeScript Monorepos.
1. Why use Project References?
- Faster Builds: TypeScript can check which sub-projects have changed and only rebuild those, rather than recompiling the entire codebase.
- Logical Separation: You can enforce boundaries. For example, your
frontendcode shouldn't importbackendcode, or yourutilslibrary shouldn't importapplogic. - Editor Performance: IDEs can load smaller projects faster.
2. Enabling Project References
To turn a folder into a referenced project, you need to enable the composite setting in its tsconfig.json.
The composite flag
Setting "composite": true ensures that TypeScript can quickly determine where to find the outputs of the referenced project.
// packages/shared/tsconfig.json
{
"compilerOptions": {
"composite": true,
"declaration": true, /* Required for composite projects */
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"]
}
3. Structuring a Monorepo
Imagine a directory structure like this:
my-monorepo/
tsconfig.json (Root config)
packages/
shared/
tsconfig.json
src/
backend/
tsconfig.json
src/
frontend/
tsconfig.json
src/
The Root tsconfig.json
The root config usually acts as a "solution" file. It doesn't contain source code itself but references the sub-projects.
// tsconfig.json (Root)
{
"files": [],
"references": [
{ "path": "./packages/shared" },
{ "path": "./packages/backend" },
{ "path": "./packages/frontend" }
]
}
Dependent Projects
If backend depends on shared, you must declare this in backend/tsconfig.json.
// packages/backend/tsconfig.json
{
"compilerOptions": {
"composite": true,
"outDir": "./dist",
"rootDir": "./src"
},
"references": [
{ "path": "../shared" }
],
"include": ["src/**/*"]
}
Now, inside backend/src/index.ts, you can import from shared. TypeScript knows to look at the output (.d.ts files) of shared rather than recompiling its source code every time backend is built.
4. Build Mode (tsc -b)
To build a project with references, you cannot use the standard tsc command. You must use the Build Mode flag: -b or --build.
# Build the entire solution from the root
tsc -b
# Build a specific package and its dependencies
tsc -b packages/backend
# Clean build artifacts
tsc -b --clean
tsc -b is smarter than tsc. It checks modification timestamps and configuration files to decide what needs to be rebuilt.
5. Best Practices
- Use Path Aliases: Often combined with
pathsincompilerOptionsso you can import likeimport { util } from '@my-app/shared'instead of../../shared/dist. - Output Structure: Ensure
outDiris configured correctly so compiled files don't clutter source folders. - Declaration Maps: Enable
"declarationMap": true. This allows "Go to Definition" in your IDE to jump to the original.tssource file in the referenced project, rather than the generated.d.tsfile.
programming/javascript/typescript/typescript programming/javascript/typescript/tsconfig