Monorepo with TypeScript: efficient code organization for scalable projects
Modern application development often requires splitting code into several independent modules or packages. This raises a dilemma: store each package in a separate repository (polyrepo) or combine everything in one (monorepo). For TypeScript projects, a monorepo becomes an especially attractive solution, providing type consistency, simplified refactoring, and centralized dependency management. In this article, we will break down how to organize a monorepo with TypeScript, configure it, and use it effectively in development.
What is a monorepo and why is it needed for TypeScript?
A monorepo is an approach where the code of several projects (packages, libraries, microservices) is stored in a single repository. In the context of TypeScript, this is particularly useful because:
- Shared types and interfaces: You can define common types in one package and reuse them across all others, avoiding duplication and desynchronization.
- Unified configuration:
tsconfig.json, linters, and formatters are configured once for the entire project. - Simplified refactoring: Changes to shared code or interfaces are immediately reflected in all dependent packages, and TypeScript will flag inconsistencies.
- Atomic commits: Changes affecting multiple packages can be committed and pushed in a single operation.
However, a monorepo requires proper organization, otherwise it turns into chaos. The key tools for working with monorepos in TypeScript are npm/yarn/pnpm workspaces and TypeScript Project References.
Structure of a monorepo with TypeScript
Let's consider a typical monorepo structure for a web application consisting of a frontend, backend, and a shared utility library:
my-monorepo/├── packages/│ ├── shared/ # Shared types, utilities, constants│ │ ├── src/│ │ │ └── index.ts│ │ ├── tsconfig.json│ │ └── package.json│ ├── backend/ # Server application (e.g., Express)│ │ ├── src/│ │ │ └── app.ts│ │ ├── tsconfig.json│ │ └── package.json│ └── frontend/ # Client application (e.g., React)│ ├── src/│ │ └── App.tsx│ ├── tsconfig.json│ └── package.json├── tsconfig.base.json # Base TypeScript configuration file├── package.json # Root package.json (workspaces)└── .gitignoreIn this structure:
packages/*— directory containing all project packages.tsconfig.base.json— shared TypeScript configuration inherited by all packages.package.jsonat the root declares workspaces.
Setting up a monorepo: step-by-step guide
1. Initialize the root project and configure Workspaces
Create the root directory and package.json file:
{ "name": "my-monorepo", "private": true, "workspaces": [ "packages/*" ], "scripts": { "build": "npm run build --workspaces", "lint": "eslint packages/*/src --ext .ts,.tsx", "test": "jest" }}The "private": true parameter prevents accidental publication of the root package. "workspaces": ["packages/*"] tells npm where to look for child packages.
2. Create a base TypeScript configuration
File tsconfig.base.json at the project root:
{ "compilerOptions": { "target": "ES2020", "module": "ESNext", "moduleResolution": "node", "strict": true, "esModuleInterop": true, "ski