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
compositesetting to thesharedproject. 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
referencesand remove this folder frominclude. You also need to add theoutDirandemitsettings 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.