Monorepo com TypeScript e Turborepo
1. Fundamentos do Monorepo com TypeScript
Um monorepo é uma estratégia de gerenciamento de código onde múltiplos projetos (pacotes, aplicações, bibliotecas) residem em um único repositório. Quando combinado com TypeScript, o monorepo oferece vantagens significativas: compartilhamento direto de tipos, interfaces e configurações entre pacotes sem a necessidade de publicação intermediária.
Vantagens principais:
- Tipos e interfaces compartilhados em tempo real durante o desenvolvimento
- Configurações centralizadas (tsconfig, ESLint, Prettier)
- Refatoração cross-package com segurança de tipos
- Versionamento atômico de mudanças interdependentes
Desafios comuns:
- Compilação incremental e cache entre pacotes
- Dependências circulares entre módulos TypeScript
- Isolamento de pacotes para evitar vazamento de tipos internos
O Turborepo resolve esses desafios com pipeline inteligente, cache distribuído e execução paralela de tarefas.
2. Configuração Inicial do Turborepo
Para iniciar, crie um novo monorepo com Turborepo:
// Terminal
npx create-turbo@latest meu-monorepo
cd meu-monorepo
A estrutura gerada segue o padrão:
meu-monorepo/
├── apps/
│ ├── web/ # Aplicação Next.js
│ └── api/ # Servidor Node.js
├── packages/
│ ├── ui/ # Componentes compartilhados
│ ├── utils/ # Utilitários TypeScript
│ └── tsconfig/ # Configurações base
├── tooling/
│ └── eslint/ # Configuração ESLint
├── turbo.json # Pipeline do Turborepo
└── package.json # Raiz com workspaces
Configuração inicial do turbo.json:
{
"$schema": "https://turbo.build/schema.json",
"globalDependencies": ["**/.env.*local"],
"pipeline": {
"build": {
"dependsOn": ["^build"],
"outputs": [".next/**", "dist/**"]
},
"typecheck": {
"dependsOn": ["^build"],
"outputs": []
},
"lint": {
"outputs": []
},
"dev": {
"cache": false,
"persistent": true
}
}
}
3. Gerenciamento de Pacotes TypeScript com npm Workspaces
Configure o package.json raiz para usar workspaces:
{
"name": "meu-monorepo",
"private": true,
"workspaces": [
"apps/*",
"packages/*",
"tooling/*"
],
"scripts": {
"dev": "turbo run dev",
"build": "turbo run build",
"typecheck": "turbo run typecheck"
},
"devDependencies": {
"turbo": "^1.10.0"
}
}
Para consumir pacotes internos, use o padrão de escopo:
// packages/ui/package.json
{
"name": "@meu-projeto/ui",
"version": "0.0.0",
"main": "./dist/index.js",
"types": "./dist/index.d.ts"
}
// apps/web/package.json
{
"name": "web",
"dependencies": {
"@meu-projeto/ui": "*",
"@meu-projeto/utils": "*"
}
}
A resolução de dependências entre pacotes TypeScript funciona porque o npm workspaces cria symlinks na node_modules raiz.
4. Compilação e Tipagem com TypeScript Project References
Configure um tsconfig.json base:
// packages/tsconfig/base.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"composite": true,
"declarationMap": true,
"declaration": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src"
}
}
Cada pacote estende a configuração base e define suas referências:
// packages/utils/tsconfig.json
{
"extends": "@meu-projeto/tsconfig/base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"],
"references": [
{ "path": "../tsconfig" }
]
}
// apps/web/tsconfig.json
{
"extends": "@meu-projeto/tsconfig/base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"],
"references": [
{ "path": "../../packages/utils" },
{ "path": "../../packages/ui" }
]
}
Integre com Turborepo no pipeline:
// turbo.json
{
"pipeline": {
"build": {
"dependsOn": ["^build"],
"inputs": ["src/**", "tsconfig.json"],
"outputs": ["dist/**"]
},
"typecheck": {
"dependsOn": ["^build"],
"inputs": ["src/**", "tsconfig.json"]
}
}
}
5. Compartilhando Configurações e Utilitários TypeScript
Crie um pacote de configuração TypeScript:
// packages/tsconfig/package.json
{
"name": "@meu-projeto/tsconfig",
"version": "0.0.0",
"files": ["base.json", "nextjs.json", "node.json"]
}
Pacote de regras ESLint com TypeScript:
// tooling/eslint/package.json
{
"name": "@meu-projeto/eslint-config",
"main": "index.js",
"dependencies": {
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0"
}
}
Utilitários compartilhados:
// packages/utils/src/helpers.ts
export type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
export function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
// packages/utils/src/index.ts
export * from './helpers';
6. Workflow de Desenvolvimento e Build
Scripts no turbo.json para desenvolvimento:
{
"pipeline": {
"dev": {
"cache": false,
"persistent": true,
"dependsOn": ["^build"]
},
"build": {
"dependsOn": ["^build"],
"outputs": ["dist/**", ".next/**"],
"cache": {
"inputs": ["src/**", "tsconfig.json", "package.json"]
}
},
"lint": {
"dependsOn": ["^build"],
"outputs": []
},
"typecheck": {
"dependsOn": ["^build"],
"outputs": []
}
}
}
O cache inteligente do Turborepo evita recompilações desnecessárias:
// Terminal - primeira execução (compila tudo)
turbo run build
// Terminal - segunda execução (usa cache se nada mudou)
turbo run build # Output: "cache hit, replaying output..."
Execução paralela e ordenada:
// Terminal
turbo run build --parallel # Executa builds em paralelo onde possível
turbo run typecheck --continue # Continua mesmo com erros em alguns pacotes
7. Publicação e Versionamento
Configure Changesets para versionamento semântico:
// .changeset/config.json
{
"$schema": "https://unpkg.com/@changesets/config@2.0.0/schema.json",
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [],
"linked": [],
"access": "public",
"baseBranch": "main",
"updateInternalDependencies": "patch",
"ignore": []
}
Script de publicação com Turborepo:
// package.json
{
"scripts": {
"release": "turbo run build --filter=./packages/* && changeset publish",
"version": "changeset version && turbo run build --filter=./packages/*"
}
}
Para publicar apenas pacotes alterados:
// Terminal
turbo run build --filter=./packages/* --filter=[HEAD^1]
changeset publish
8. Boas Práticas e Armadilhas Comuns
Evitando dependências circulares:
// ❌ Ruim: pacote A depende de B, B depende de A
// packages/a/src/index.ts
import { something } from '@meu-projeto/b';
// packages/b/src/index.ts
import { something } from '@meu-projeto/a';
// ✅ Bom: extrair interface comum para pacote compartilhado
// packages/types/src/interfaces.ts
export interface SharedInterface {
// ...
}
Consistência de versões do TypeScript:
// package.json raiz
{
"devDependencies": {
"typescript": "^5.3.0"
},
"resolutions": {
"typescript": "^5.3.0"
}
}
Debugging de tipos entre pacotes:
// Terminal - rastrear resolução de módulos
npx tsc --traceResolution
// Terminal - explicar arquivos incluídos na compilação
npx tsc --explainFiles
// Terminal - verificar tipos em pacote específico
turbo run typecheck --filter=@meu-projeto/utils
Isolamento de pacotes:
// packages/utils/package.json
{
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": ["dist"],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
}
Com essas práticas, seu monorepo TypeScript com Turborepo será escalável, rápido e seguro para times de qualquer tamanho.
Referências
- Documentação oficial do Turborepo — Guia completo de configuração, pipeline e cache do Turborepo
- Documentação do TypeScript Project References — Como configurar compilação incremental entre pacotes
- npm Workspaces Documentation — Gerenciamento de múltiplos pacotes no mesmo repositório
- Changesets: Guia de Versionamento — Ferramenta para versionamento semântico e changelog automatizado
- Turborepo: Boas Práticas de Cache — Como otimizar o cache para builds mais rápidos
- TypeScript Handbook: Module Resolution — Entendendo a resolução de módulos para debugging