This is an alpha, sneak peek of Monorepo Maestros. For this iteration, I'm getting all of my thoughts down. In the future, we'll have better information architecture, graphics, and other awesomeness. Your feedback is welcome!
TypeScript
Handling TypeScript in a monorepo can be daunting at first. If you set it up wrong, you can end up frustrated with errors that don't make sense and slow type checking scripts.
But, once you've learned to hold the TypeScript violin just right, the type checking in your repo will be fast and you can iterate safely.
Let's build a simple TypeScript package to find out how this works.
First, we'll build a TSConfig that will be the base for all the TypeScript code in our repository. We'll always extend off of this base to reduce duplication and know we're working with the right set of defaults every time.
Create a workspace in your tooling
directory (or packages
, whatever you prefer). We'll add a package.json
that exports a base.json
file:
tooling/tsconfig/package.json
"name": "@repo/tsconfig",
"version": "0.0.0",
"private": true,
"files": ["base.json"]
tooling/tsconfig/package.json
"name": "@repo/tsconfig",
"version": "0.0.0",
"private": true,
"files": ["base.json"]
tooling/tsconfig/package.json
"name": "@repo/tsconfig",
"version": "0.0.0",
"private": true,
"files": ["base.json"]
tooling/tsconfig/package.json
"name": "@repo/tsconfig",
"version": "0.0.0",
"private": true,
"files": ["base.json"]
Note that we don't need to install TypeScript (or anything else!) in this package's dependencies. All we need is a base.json
TSConfig like this one:
tooling/tsconfig/base.json
{
"$schema": "https://json.schemastore.org/tsconfig", // For in-editor Intellisense
"display": "Default",
"compilerOptions": {
"incremental": true, // Must have for next steps!
"skipLibCheck": true, // Highly recommended!
"strict": true, // Highly recommended!
"composite": false,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"inlineSources": false,
"isolatedModules": true,
"moduleResolution": "Node16",
"noImplicitAny": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"preserveWatchOutput": true
},
"exclude": ["node_modules"]
}
tooling/tsconfig/base.json
{
"$schema": "https://json.schemastore.org/tsconfig", // For in-editor Intellisense
"display": "Default",
"compilerOptions": {
"incremental": true, // Must have for next steps!
"skipLibCheck": true, // Highly recommended!
"strict": true, // Highly recommended!
"composite": false,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"inlineSources": false,
"isolatedModules": true,
"moduleResolution": "Node16",
"noImplicitAny": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"preserveWatchOutput": true
},
"exclude": ["node_modules"]
}
tooling/tsconfig/base.json
{
"$schema": "https://json.schemastore.org/tsconfig", // For in-editor Intellisense
"display": "Default",
"compilerOptions": {
"incremental": true, // Must have for next steps!
"skipLibCheck": true, // Highly recommended!
"strict": true, // Highly recommended!
"composite": false,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"inlineSources": false,
"isolatedModules": true,
"moduleResolution": "Node16",
"noImplicitAny": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"preserveWatchOutput": true
},
"exclude": ["node_modules"]
}
tooling/tsconfig/base.json
{
"$schema": "https://json.schemastore.org/tsconfig", // For in-editor Intellisense
"display": "Default",
"compilerOptions": {
"incremental": true, // Must have for next steps!
"skipLibCheck": true, // Highly recommended!
"strict": true, // Highly recommended!
"composite": false,
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"inlineSources": false,
"isolatedModules": true,
"moduleResolution": "Node16",
"noImplicitAny": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"preserveWatchOutput": true
},
"exclude": ["node_modules"]
}
The TSConfig above has the usual defaults for most projects so feel free to copy-paste. It contains many optional properties that you can tailor it to your liking but, at the very least, make sure you keep:
"strict": true
so the compiler finds as many mistakes as possible
"skipLibCheck": true
so only the source code in your workspace is checked by the task
"incremental": true
for faster type checking (which we will discuss in a moment)
There are three strategies that we will use to make tsc
fast:
incremental
saves information about your project compilation into a file. The first time you run tsc
, the compiler will create this file to make subsequent runs faster.
- Parallelizing the type checking scripts in our workspaces.
- Caching workspace tasks with our package manager so that we can ensure we never do the same work twice.
Putting these techniques together, we'll be caching the type check task for entire workspaces so we hit cache for work we've already done and be as fast as possible when we miss the cache to check types.
Let's start by handling the package.json
for our workspace. We'll need to install:
typescript
@repo/tsconfig
, the package with our base TSConfig.
After that we'll create a script for running tsc
with the --noEmit
flag so that the compiler only checks types, skipping out on the actual compilation work.
packages/logger/package.json
{
"scripts": {
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@repo/tsconfig": "workspace:*" // Use "*" for npm or yarn
},
"devDependencies": {
"typescript": "5.1.6"
}
}
packages/logger/package.json
{
"scripts": {
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@repo/tsconfig": "workspace:*" // Use "*" for npm or yarn
},
"devDependencies": {
"typescript": "5.1.6"
}
}
packages/logger/package.json
{
"scripts": {
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@repo/tsconfig": "workspace:*" // Use "*" for npm or yarn
},
"devDependencies": {
"typescript": "5.1.6"
}
}
packages/logger/package.json
{
"scripts": {
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@repo/tsconfig": "workspace:*" // Use "*" for npm or yarn
},
"devDependencies": {
"typescript": "5.1.6"
}
}
Next, we'll create a tsconfig.json
in our workspace where we'll extend from our base configuration:
packages/logger/tsconfig.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@repo/tsconfig/base.json", // Using our base config!
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["."],
"exclude": ["node_modules"]
}
packages/logger/tsconfig.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@repo/tsconfig/base.json", // Using our base config!
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["."],
"exclude": ["node_modules"]
}
packages/logger/tsconfig.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@repo/tsconfig/base.json", // Using our base config!
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["."],
"exclude": ["node_modules"]
}
packages/logger/tsconfig.json
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@repo/tsconfig/base.json", // Using our base config!
"compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json"
},
"include": ["."],
"exclude": ["node_modules"]
}
Most of this file probably looks familiar. We're extending from our base TSConfig - but what's this tsBuildInfoFile
property?
tsBuildInfoFile
tells the TypeScript compiler where to store the output of incremental
. We're going to use the .cache
folder in node_modules
as this is a common practice for storing caches for libraries and dependencies.
Test it out! (Using globally installed turbo
for simplicity here)
cd packages/logger
turbo typecheck
cd packages/logger
turbo typecheck
cd packages/logger
turbo typecheck
cd packages/logger
turbo typecheck
If all goes according to plan, you'll see your task ran by Turborepo in your terminal and a tsbuildinfo.json
file in the node_modules/.cache
directory of your workspace.
Now, we'll build our Turborepo pipeline with two important characteristics.
{
"pipeline": {
"topo": {
"dependsOn": ["^topo"]
},
"typecheck": {
"dependsOn": ["topo"],
"outputs": ["node_modules/.cache/tsbuildinfo.json"]
}
}
}
{
"pipeline": {
"topo": {
"dependsOn": ["^topo"]
},
"typecheck": {
"dependsOn": ["topo"],
"outputs": ["node_modules/.cache/tsbuildinfo.json"]
}
}
}
{
"pipeline": {
"topo": {
"dependsOn": ["^topo"]
},
"typecheck": {
"dependsOn": ["topo"],
"outputs": ["node_modules/.cache/tsbuildinfo.json"]
}
}
}
{
"pipeline": {
"topo": {
"dependsOn": ["^topo"]
},
"typecheck": {
"dependsOn": ["topo"],
"outputs": ["node_modules/.cache/tsbuildinfo.json"]
}
}
}
- Recursively depending on
topo
: This is a little trick from the Turborepo documentation. In short, this dependsOn
pattern flattens your task graph so that everything runs in parallel while still respecting changes in workspace dependencies. (If that sounds confusing, don't worry about it for now; just trust that it works. We'll be writing up a doc for this but, for the time being, let it be magic.) ✨
- Caching the output of
incremental
: In the event that our task misses cache, we will still have the tsbuildinfo.json
to use to speed up our task. Caching this file as a Turborepo output
ensures that we have the tsbuildinfo.json
shared across our machines as often as possible so we can use it in as many places as we can.
With these two optimizations, we've massively sped up the type checking task in your repository. We also have a base TSConfig that we will always use so our repository has uniformity that we can trust between individuals and teams.
Congratulations, you are now friends with TypeScript.
Good to know:
You can create multiple files in your TSConfig workspace to extend from. You
may want to do this if you have specific needs (for instance, a certain
framework requires certain options). If you do create multiple files, you
may still find it advantageous to extend from a common base configuration in
those custom configurations..
You'll notice I didn't mention TypeScript's Project References. In a
Turborepo, you can usually avoid them. 😁