Using TypeScript in a monorepo
I usually try not to write about things already covered in documentation.
However, monorepo configuration in TypeScript always seemed complicated to me. I also noticed that many projects I have seen suffer from the same problem — the configuration is not optimal and could be improved.
Well, it’s actually much easier than I thought.
For example, I have the following project structure:
tsconfig.json
packages
├── app1
│ ├── src
│ └── tsconfig.json
├── app2
│ ├── src
│ └── tsconfig.json
├── shared
│ ├── src
│ └── tsconfig.json
Where shared
is used by both app1
and app2
.
Previously, I had a configuration that looked like this:
// packages/app1/tsconfig.json
{
"include": ["src", "../shared/src"]
}
// packages/app2/tsconfig.json
{
"include": ["src", "../shared/src"]
}
// packages/shared/tsconfig.json
{
"include": ["src"]
}
Notice this include
part, which exists in both apps. For TypeScript, it basically means that every time it checks one of these projects, it includes shared
in the check.
To run checks for these projects, I used the following commands:
tsc -p packages/app1
and
tsc -p packages/app2
You have probably already figured out where the problem is. When I run the first command, both packages/app1
and packages/shared
are checked. When I run the second command, both packages/app2
and packages/shared
are checked.
How can I avoid wasting time compiling the same folder twice? It turned out that everything is much simpler than I thought.
I needed to do some modifications:
- Add the
composite
setting to theshared
project. This means that the project will be used as part of another.
// packages/shared/tsconfig.json
{
"compilerOptions": {
"outDir": "dist",
"emitDeclarationOnly": true,
"composite": true
},
"include": ["src"]
}
- Add
references
and remove this folder frominclude
. You also need to add theoutDir
andemit
settings if you haven’t done so already.
// packages/app1/tsconfig.json and packages/app2/tsconfig.json
{
"compilerOptions": {
"outDir": "dist",
"emit": true
},
"references": [
{
"path": "../shared"
}
]
}
You can safely add this dist
folder to your .gitignore
and hide it in your code editor. You don’t need these files while writing code, as your code editor will generate them automatically.
After this changes shared
folder will be compiled only once.
And here is bonus tip. You can create the following TS config in the root of your project:
// tsconfig.json
{
"files": [],
"references": [
{
"path": "packages/app1"
},
{
"path": "packages/app2"
},
{
"path": "packages/shared"
}
]
}
and run check with command (please note that it uses -b
flag instead of -p
):
tsc -b tsconfig.json
With this command TS will automatically run typechecking for all referenced projects, but your composite
folders will be checked only once.
With modifications described I was able to optimize build time from 49s to 13s.
Most important note: If you have a monorepo where each package is independent (i.e., it doesn’t use other packages) or an application that uses all packages at once (making it just a monorepo for the sake of having a monorepo), you DON’T NEED such a configuration. You should probably use the include
option.