- Published on
Scaling TypeScript Monorepos: Catalog Management + Turbo Generators
- Authors

- Name
- Jacek Smolak
- @jacek_smolak
Scaling TypeScript Monorepos: Catalog Management + Turbo Generators
How I eliminated version chaos and automated package creation in my TypeScript monorepo
The Problem: Monorepo Growing Pains
As my TypeScript monorepo grew from a few packages to dozens, I faced two critical challenges:
- Version Hell: Different packages using different versions of the same dependencies, leading to inconsistent behavior and security vulnerabilities.
- Manual Setup Overhead: Creating new packages required copying boilerplate, remembering all the right configurations, and hoping I didn't miss anything.
Sound familiar? If you're managing a TypeScript monorepo, you've likely encountered these issues. Here's how I solved them.
Solution 1: Centralized Dependency Management with PNPM Catalogs
The Challenge
My package.json files looked like this:
// packages/ui/package.json
{
"devDependencies": {
"typescript": "^5.0.0",
"vitest": "^3.1.0"
}
}
// packages/shared/package.json
{
"devDependencies": {
"typescript": "^5.1.0", // Different version!
"vitest": "^3.2.0" // Different version!
}
}
The Solution: PNPM Catalogs
I implemented PNPM's catalog feature to centralize all dependency versions:
# pnpm-workspace.yaml
catalog:
typescript: "5.9.2"
vitest: "^3.2.4"
jest-extended: "^6.0.0"
@types/node: "^22.18.1"
Now all packages reference the catalog:
// Any package.json
{
"devDependencies": {
"typescript": "catalog:",
"vitest": "catalog:"
}
}
Benefits
- ✅ Single source of truth for all versions
- ✅ Consistent behavior across all packages
- ✅ Easy updates - change once, update everywhere
- ✅ Security - no forgotten outdated dependencies
Solution 2: Automated Package Generation with Turbo Generators
The Challenge
Creating a new package meant:
- Creating the directory structure (using turbo generator)
- Writing
package.jsonwith correct dependencies - Setting up
tsconfig.jsonwith proper paths - Adding
vitest.config.tsif tests were needed - Remembering all the boilerplate...
And I'd inevitably forget something or make mistakes.
The Solution: Turbo Generators
I built a custom generator using Turbo's generator system:
// turbo/generators/config.ts
plop.setGenerator('workspace', {
description: 'Create a new workspace with @repo prefix',
prompts: [
{
type: 'input',
name: 'name',
message: 'Workspace name (without @repo prefix):',
},
{
type: 'list',
name: 'type',
message: 'What type of workspace?',
choices: [
{ name: 'Package', value: 'package' },
{ name: 'App', value: 'app' },
],
},
{
type: 'confirm',
name: 'hasTests',
message: 'Will this workspace include tests?',
default: true,
},
],
// ... actions
})
Smart Templates
My templates automatically configure everything based on the package type and requirements:
Package with tests:
// Generated package.json
{
"name": "@repo/new-package",
"scripts": {
"test": "vitest run" // Auto-configured!
},
"devDependencies": {
"vitest": "catalog:" // Uses catalog version
}
}
// Generated tsconfig.json
{
"compilerOptions": {
"types": ["vitest/globals"]
},
"include": [
"src/**/*",
"../tests-setup/src/vitest-jest-extended.d.ts"
]
}
// Generated vitest.config.ts
export default defineConfig({
test: {
globals: true,
setupFiles: '../tests-setup/src/index.ts', // Shared test setup
},
})
The Complete Setup: Jest Extended + Vitest
As a bonus, I also solved TypeScript recognition for Jest Extended matchers:
The Problem
expect(id).toBeString() // ❌ TypeScript error: Property 'toBeString' does not exist
The Solution
// packages/tests-setup/src/vitest-jest-extended.d.ts
declare module 'vitest' {
interface Assertion<T = unknown> extends jestExtended.Matchers<T> {}
interface AsymmetricMatchersContaining extends jestExtended.Matchers {}
}
Now TypeScript recognizes all Jest Extended matchers! 🎉
Results: Developer Experience Revolution
Before
- 🔴 Inconsistent dependency versions
- 🔴 Manual package setup (minutes per package)
- 🔴 Easy to forget configurations
- 🔴 TypeScript errors with test matchers
After
- ✅ All packages use identical dependency versions
- ✅ New package creation (seconds)
- ✅ Zero configuration mistakes
- ✅ Full TypeScript support for all test matchers
Key Takeaways
- Catalogs eliminate version chaos - One place to manage all dependency versions
- Generators eliminate setup overhead - Automated, consistent package creation
- Templates are powerful - Conditional logic based on package requirements
- Developer experience matters - Small improvements compound over time
What's Next?
This setup has transformed how I work with my monorepo. New packages are created in seconds, not minutes. Dependencies are always consistent. And I can focus on building features instead of fighting configuration.
Consider implementing these patterns in your own monorepo. The time investment pays dividends as your team and codebase grow.