TypeScript Project References & Monorepos
Quick Overview
Project references let each package build and cache independently, so touching one does not recompile the monorepo. `composite` emits the `.d.ts` files the other projects consume in place of source.
Deep Dive
1. Scaling Large Codebases
In large enterprise codebases and monorepos, compiling all source files as a single monolithic program slows down build times. Project References allow you to structure TypeScript code into independent, interlinked modules.
2. Enabling Composite Builds
To allow a project to be referenced by another tsconfig.json, enable "composite": true in the child project's configuration:
// packages/core/tsconfig.json
{
"compilerOptions": {
"composite": true,
"declaration": true,
"rootDir": "src",
"outDir": "dist"
}
}
3. Referencing Sub-Projects
Reference child projects in the root or consuming app tsconfig.json:
// apps/web/tsconfig.json
{
"compilerOptions": {
"target": "ES2022"
},
"references": [
{ "path": "../../packages/core" }
]
}
4. Building with Build Mode (tsc -b)
Use TypeScript's build mode (tsc -b) to compile referenced projects incrementally:
# Build referenced projects incrementally in correct dependency order!
tsc -b apps/web
5. Architectural Monorepo Benefits
- Faster Incremental Builds: TypeScript skips re-compiling referenced projects whose
.d.tsoutputs have not changed. - Enforced Architectural Boundaries: Prevents illegal circular imports between separate packages in a monorepo.
6. Common Mistakes
- Forgetting declaration: true in Composite Projects: Composite project references require
declaration: trueto generate build declaration maps for consumer projects.
7. Congratulations!
Congratulations! You have completed all 30 foundational TypeScript Learning Bytes, mastering everything from primitive type annotations to generics, conditional types, custom utility types, and enterprise monorepo compilation!
How was this byte?
Learning Progress
30 of 30Next Up in TypeScript & Type System Mechanics
TypeScript Primitives & Explicit Type Annotations
TypeScript erases entirely at build time, so a `string` annotation produces no runtime check whatsoever. A value arriving from an API is whatever the server actually sent, regardless of what the annotation claims.
Type Inference & Literal Types (const vs. let)
`let x = "GET"` infers `string` because the binding can change; `const x = "GET"` infers the literal `"GET"`. That widening rule is why object properties need `as const` to stay narrow.
Arrays, Tuples & Readonly Modifiers
`string[]` is any number of strings; `[string, number]` is exactly two, in that order. `readonly` removes the mutating methods at compile time, so `push` becomes a type error rather than a runtime surprise.
