Project references: monorepos com TypeScript
1. Introdução às Project References no TypeScript
Project References foram introduzidas no TypeScript 3.0 para resolver um problema clássico em monorepos: como gerenciar múltiplos projetos TypeScript que dependem uns dos outros de forma eficiente. Antes dessa funcionalidade, desenvolvedores precisavam usar um único tsconfig.json gigante, compilar tudo de uma vez ou recorrer a ferramentas externas.
Os principais problemas resolvidos incluem:
- Builds incrementais: apenas projetos alterados são recompilados
- Isolamento de módulos: cada projeto tem seu próprio escopo e configuração
- Dependências explícitas: fica claro qual projeto depende de qual
Diferente de um monorepo com tsconfig único, onde qualquer mudança força uma recompilação completa, Project References permitem que o TypeScript entenda a hierarquia de dependências e compile apenas o necessário.
2. Configuração Básica de um Monorepo com Project References
Vamos criar uma estrutura simples de monorepo:
monorepo/
├── packages/
│ ├── core/
│ │ ├── src/
│ │ │ └── index.ts
│ │ └── tsconfig.json
│ └── utils/
│ ├── src/
│ │ └── index.ts
│ └── tsconfig.json
├── tsconfig.json
└── package.json
O tsconfig.json raiz referencia os subprojetos:
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true
},
"references": [
{ "path": "./packages/core" },
{ "path": "./packages/utils" }
]
}
Cada subprojeto deve ter composite: true, declaration: true e declarationMap:
// packages/core/tsconfig.json
{
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}
3. Gerenciamento de Dependências Entre Projetos
Para que packages/utils possa usar packages/core, configuramos as referências:
// packages/utils/tsconfig.json
{
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src"
},
"references": [
{ "path": "../core" }
],
"include": ["src"]
}
E no código:
// packages/utils/src/index.ts
import { coreFunction } from '@monorepo/core';
export function utilsFunction(): string {
return `Utils usando: ${coreFunction()}`;
}
Para resolver os imports corretamente, usamos paths no tsconfig.json raiz:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@monorepo/core": ["packages/core/src"],
"@monorepo/utils": ["packages/utils/src"]
}
}
}
4. Build Incremental e Cache Inteligente
O comando tsc --build (ou -b) é a chave para builds incrementais:
# Compila tudo
tsc --build
# Compila apenas projetos alterados desde a última build
tsc --build
# Força recompilação de um projeto específico
tsc --build packages/core
# Limpa e reconstrói
tsc --build --clean
tsc --build
O TypeScript gera arquivos .tsbuildinfo que armazenam timestamps e hashes dos arquivos compilados. Exemplo de como isso funciona:
// packages/core/src/index.ts
export function coreFunction(): string {
return 'Core v1.0';
}
Após a primeira compilação, o arquivo packages/core/tsconfig.tsbuildinfo é criado. Se apenas packages/utils for alterado, apenas ele será recompilado.
5. Integração com Ferramentas de Monorepo
Combinando Project References com npm workspaces no package.json:
// package.json
{
"name": "monorepo",
"private": true,
"workspaces": [
"packages/*"
],
"scripts": {
"build": "tsc --build",
"clean": "tsc --build --clean",
"watch": "tsc --build --watch"
}
}
Para sincronizar automaticamente as referências, podemos usar scripts:
// scripts/sync-references.ts
import { readFileSync, writeFileSync } from 'fs';
import { glob } from 'glob';
const packages = glob.sync('packages/*/package.json');
const references = packages.map(pkg => ({
path: `../${pkg.replace('/package.json', '')}`
}));
const tsconfig = JSON.parse(readFileSync('tsconfig.json', 'utf-8'));
tsconfig.references = references;
writeFileSync('tsconfig.json', JSON.stringify(tsconfig, null, 2));
Exemplo com pnpm:
pnpm add -w typescript
pnpm exec tsc --build
6. Testes, Linting e Ferramentas no Monorepo
Configurando Jest com ts-jest e Project References:
// jest.config.ts
export default {
preset: 'ts-jest',
testEnvironment: 'node',
globals: {
'ts-jest': {
tsconfig: 'tsconfig.json'
}
}
};
Para ESLint em múltiplos projetos:
// .eslintrc.js
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
parserOptions: {
project: ['./tsconfig.json', './packages/*/tsconfig.json']
},
plugins: ['@typescript-eslint'],
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended']
};
Usando Turbo para executar scripts em ordem:
npx turbo run build --filter=./packages/*
7. Erros Comuns e Como Resolver
Erro: "Cannot find module"
// packages/utils/src/index.ts
import { coreFunction } from '@monorepo/core';
// Error: Cannot find module '@monorepo/core'
Solução: Verifique se o tsconfig.json de utils referencia core corretamente e se os paths estão configurados.
Erro: "File is not under 'rootDir'"
// packages/core/tsconfig.json
{
"compilerOptions": {
"rootDir": "./src"
}
}
Se você tentar importar algo fora de src, receberá esse erro. Solução: ajuste o rootDir ou mova os arquivos.
Problemas com declarationMap: Para depuração entre projetos, certifique-se de que declarationMap: true está em todos os projetos. Isso permite que o VS Code navegue diretamente para o código fonte ao invés dos arquivos .d.ts.
Build incremental não funciona: Verifique se os timestamps dos arquivos .tsbuildinfo são mais recentes que os arquivos fonte. Use --force para reconstruir:
tsc --build --force
8. Boas Práticas e Padrões Avançados
Organize projetos por camadas:
packages/
├── core/ # Dependências base
├── utils/ # Utilitários dependendo de core
└── app/ # Aplicação final dependendo de utils
Use baseUrl e paths de forma consistente em todos os projetos:
// packages/app/tsconfig.json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@monorepo/core": ["../core/src"],
"@monorepo/utils": ["../utils/src"]
}
}
}
Automatize a manutenção de referências com ferramentas como tsc-multi ou scripts customizados:
// scripts/update-references.ts
import { execSync } from 'child_process';
const packages = ['core', 'utils', 'app'];
packages.forEach(pkg => {
execSync(`tsc --build packages/${pkg} --dry`, { stdio: 'inherit' });
});
Conclusão
Project References transformam o TypeScript em uma ferramenta robusta para monorepos, oferecendo builds incrementais, isolamento de dependências e integração com ecossistemas modernos. Ao dominar essa funcionalidade, você ganha performance e organização em projetos de qualquer escala.
Referências
- Documentação oficial do TypeScript sobre Project References — Guia completo com exemplos e explicações detalhadas sobre configuração e uso
- TypeScript 3.0 Release Notes - Project References — Notas de lançamento oficiais que introduziram a funcionalidade
- Monorepos with TypeScript Project References - LogRocket — Artigo prático mostrando implementação real em projetos
- Building a TypeScript Monorepo with Project References - DEV Community — Tutorial passo a passo com exemplos de código
- TypeScript Project References in Practice - Matt Pocock — Guia avançado com dicas de otimização e resolução de problemas comuns
- Using TypeScript Project References with pnpm Workspaces — Documentação oficial do pnpm sobre integração com TypeScript Project References
- How to Set Up a TypeScript Monorepo with Project References - Sitepoint — Tutorial completo abrangendo configuração, testes e deploy