<h2>Project References: A Estrutura Fundamental do TypeScript</h2>
<p>Project References é um recurso do TypeScript que permite organizar grandes projetos em múltiplos sub-projetos (ou "workspaces"), onde cada um tem seu próprio arquivo <code>tsconfig.json</code>. Diferente de módulos tradicionais, Project References estabelece uma relação de <strong>dependência explícita</strong> entre projetos TypeScript, permitindo que o compilador entenda a hierarquia e as dependências reais do seu código. Isso é crucial para manter projetos escaláveis e com build times reduzidos.</p>
<p>Quando você trabalha sem Project References, o TypeScript compila todo o código de uma vez. Conforme seu projeto cresce, isso se torna ineficiente. Project References resolve esse problema criando uma estrutura onde cada projeto é compilado isoladamente, gerando arquivos <code>.d.ts</code> (type definitions) que servem como contrato entre os projetos. O TypeScript então usa essas definições para verificar tipos sem recompilar código que não mudou.</p>
<h3>Como Project References Funciona</h3>
<p>O mecanismo é simples: você declara referências de projeto no <code>tsconfig.json</code> usando a propriedade <code>references</code>. Quando o TypeScript vê uma referência, ele cria um "módulo virtual" que aponta para as type definitions do projeto referenciado, em vez de recompilar seu código TypeScript. Isso permite que você tenha build paralelo e incremental — apenas os projetos que sofreram mudanças são recompilados.</p>
<h2>Monorepos com TypeScript: Estrutura e Estratégia</h2>
<p>Um monorepo é um repositório único que contém múltiplos projetos logicamente independentes. TypeScript com Project References é uma abordagem excelente para monorepos, pois oferece isolamento de tipos sem duplicação de código ou dependências circulares. A estrutura típica segue um padrão onde você tem uma pasta raiz com múltiplos pacotes (packages), cada um com seu próprio <code>tsconfig.json</code> e <code>package.json</code>.</p>
<p>A vantagem principal é que você consegue compartilhar código tipo uma biblioteca interna enquanto mantém cada projeto com suas próprias dependências e configurações de compilação. Ferramentas como Yarn Workspaces, npm Workspaces e pnpm gerenciam as dependências no nível do monorepo, enquanto Project References garante que o TypeScript entenda as relações de tipo entre os projetos.</p>
<h3>Estrutura de Diretórios de um Monorepo</h3>
<pre><code>monorepo/
├── tsconfig.json # Config raiz (base)
├── package.json # Dependências compartilhadas
├── packages/
│ ├── core/
│ │ ├── tsconfig.json # Projeto core
│ │ ├── package.json
│ │ └── src/
│ │ ├── math.ts
│ │ └── utils.ts
│ ├── api/
│ │ ├── tsconfig.json # Depende de core
│ │ ├── package.json
│ │ └── src/
│ │ └── server.ts
│ └── cli/
│ ├── tsconfig.json # Depende de core
│ ├── package.json
│ └── src/
│ └── index.ts
└── dist/</code></pre>
<h3>Configurando Project References</h3>
<p>O arquivo <code>tsconfig.json</code> raiz serve como base para todos os projetos. Ele não compila código, apenas define configurações padrão que são herdadas pelos sub-projetos:</p>
<pre><code class="language-json">{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020"],
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "../dist"
},
"include": [],
"references": []
}</code></pre>
<p>Note que a raiz tem <code>include</code> e <code>references</code> vazios. Cada sub-projeto tem seu próprio <code>tsconfig.json</code> que estende a raiz e declara suas próprias referências:</p>
<pre><code class="language-json">{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/core"
},
"include": ["src"],
"references": []
}</code></pre>
<p>Para o projeto <code>api</code>, que depende de <code>core</code>, você declara a referência explicitamente:</p>
<pre><code class="language-json">{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/api"
},
"include": ["src"],
"references": [
{ "path": "../core" }
]
}</code></pre>
<h2>Builds Incrementais e Otimização de Performance</h2>
<p>Builds incrementais significam que apenas o código que mudou é recompilado. Sem Project References, cada build recompila tudo. Com eles, você consegue rastrear quais projetos foram afetados pelas mudanças e recompilar apenas esses. O TypeScript faz isso usando timestamps dos arquivos <code>.d.ts</code> gerados.</p>
<p>Quando você altera um arquivo em <code>core</code>, o TypeScript regenera os <code>.d.ts</code> de <code>core</code>. Quando você compila <code>api</code>, o TypeScript detecta que as type definitions de <code>core</code> mudaram e recompila <code>api</code>. Mas se você compilar apenas <code>core</code> sem alterar nada, a próxima compilação de <code>api</code> será instantânea porque os <code>.d.ts</code> não mudaram.</p>
<h3>Compilando com --build</h3>
<p>Para aproveitar builds incrementais, use o flag <code>--build</code> do TypeScript em vez do modo normal:</p>
<pre><code class="language-bash">tsc --build packages/core packages/api --verbose</code></pre>
<p>Esse comando compila apenas o que é necessário, respeitando as dependências. Se você modificar algo em <code>core</code>, <code>api</code> será recompilado automaticamente. O <code>--verbose</code> mostra exatamente o que foi compilado, útil para debugar.</p>
<p>Você também pode limpar builds anteriores com <code>--clean</code>:</p>
<pre><code class="language-bash">tsc --build --clean packages/core packages/api</code></pre>
<h3>Script de Build Incremental no package.json</h3>
<pre><code class="language-json">{
"scripts": {
"build": "tsc --build packages/core packages/api packages/cli",
"build:watch": "tsc --build --watch packages/core packages/api packages/cli",
"build:clean": "tsc --build --clean packages/core packages/api packages/cli",
"build:core": "tsc --build packages/core"
}
}</code></pre>
<p>O <code>--watch</code> é particularmente poderoso: mantém o processo rodando e recompila apenas os projetos afetados quando arquivos mudam. Isso torna o desenvolvimento extremamente rápido.</p>
<h2>Exemplo Prático: Um Monorepo Real</h2>
<p>Vamos criar um monorepo com três projetos: uma biblioteca de utilitários (<code>core</code>), um servidor API (<code>api</code>) e uma CLI (<code>cli</code>). O <code>api</code> e <code>cli</code> dependem de <code>core</code>.</p>
<h3>Projeto Core - Utilitários Compartilhados</h3>
<p><strong>packages/core/src/math.ts:</strong></p>
<pre><code class="language-typescript">export function add(a: number, b: number): number {
return a + b;
}
export function multiply(a: number, b: number): number {
return a * b;
}
export function divide(a: number, b: number): number {
if (b === 0) throw new Error("Division by zero");
return a / b;
}</code></pre>
<p><strong>packages/core/src/logger.ts:</strong></p>
<pre><code class="language-typescript">export interface LoggerOptions {
level: "info" | "warn" | "error";
timestamp: boolean;
}
export class Logger {
private options: LoggerOptions;
constructor(options: LoggerOptions = { level: "info", timestamp: true }) {
this.options = options;
}
log(message: string): void {
const prefix = this.options.timestamp ? [${new Date().toISOString()}] : "";
console.log(${prefix} [${this.options.level}] ${message});
}
}</code></pre>
<p><strong>packages/core/src/index.ts:</strong></p>
<pre><code class="language-typescript">export * from "./math";
export * from "./logger";</code></pre>
<p><strong>packages/core/tsconfig.json:</strong></p>
<pre><code class="language-json">{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/core"
},
"include": ["src"],
"references": []
}</code></pre>
<h3>Projeto API - Depende de Core</h3>
<p><strong>packages/api/src/server.ts:</strong></p>
<pre><code class="language-typescript">import { add, multiply, Logger } from "@monorepo/core";
const logger = new Logger({ level: "info", timestamp: true });
export interface Request {
operation: "add" | "multiply";
a: number;
b: number;
}
export function handleRequest(req: Request): number {
logger.log(Processing ${req.operation} operation);
switch (req.operation) {
case "add":
return add(req.a, req.b);
case "multiply":
return multiply(req.a, req.b);
default:
throw new Error(Unknown operation: ${req.operation});
}
}</code></pre>
<p><strong>packages/api/src/index.ts:</strong></p>
<pre><code class="language-typescript">import { handleRequest, Request } from "./server";
const result = handleRequest({ operation: "add", a: 10, b: 5 });
console.log("Result:", result);</code></pre>
<p><strong>packages/api/tsconfig.json:</strong></p>
<pre><code class="language-json">{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/api"
},
"include": ["src"],
"references": [
{ "path": "../core" }
]
}</code></pre>
<p><strong>packages/api/package.json:</strong></p>
<pre><code class="language-json">{
"name": "@monorepo/api",
"version": "1.0.0",
"dependencies": {
"@monorepo/core": "workspace:*"
}
}</code></pre>
<h3>Projeto CLI - Também Depende de Core</h3>
<p><strong>packages/cli/src/commands.ts:</strong></p>
<pre><code class="language-typescript">import { divide, Logger } from "@monorepo/core";
const logger = new Logger({ level: "info", timestamp: true });
export function executeCalculation(args: string[]): void {
if (args.length < 3) {
logger.log("Usage: cli <operation> <a> <b>");
return;
}
const [operation, aStr, bStr] = args;
const a = parseFloat(aStr);
const b = parseFloat(bStr);
if (isNaN(a) || isNaN(b)) {
logger.log("Invalid numbers provided");
return;
}
try {
if (operation === "divide") {
const result = divide(a, b);
logger.log(Result: ${result});
} else {
logger.log(Unknown operation: ${operation});
}
} catch (error) {
logger.log(Error: ${(error as Error).message});
}
}</code></pre>
<p><strong>packages/cli/src/index.ts:</strong></p>
<pre><code class="language-typescript">import { executeCalculation } from "./commands";
const args = process.argv.slice(2);
executeCalculation(args);</code></pre>
<p><strong>packages/cli/tsconfig.json:</strong></p>
<pre><code class="language-json">{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../dist/cli"
},
"include": ["src"],
"references": [
{ "path": "../core" }
]
}</code></pre>
<h3>Configuração Raiz do Monorepo</h3>
<p><strong>tsconfig.json (raiz):</strong></p>
<pre><code class="language-json">{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"lib": ["ES2020"],
"moduleResolution": "node",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"baseUrl": ".",
"paths": {
"@monorepo/core": ["packages/core/src"],
"@monorepo/api": ["packages/api/src"],
"@monorepo/cli": ["packages/cli/src"]
}
},
"include": [],
"references": [
{ "path": "packages/core" },
{ "path": "packages/api" },
{ "path": "packages/cli" }
]
}</code></pre>
<p><strong>package.json (raiz):</strong></p>
<pre><code class="language-json">{
"name": "monorepo",
"version": "1.0.0",
"workspaces": [
"packages/*"
],
"scripts": {
"build": "tsc --build",
"build:watch": "tsc --build --watch",
"build:clean": "tsc --build --clean",
"build:core": "tsc --build packages/core",
"build:api": "tsc --build packages/core packages/api",
"build:cli": "tsc --build packages/core packages/cli"
},
"devDependencies": {
"typescript": "^5.3.0"
}
}</code></pre>
<h3>Compilando e Testando</h3>
<p>Com essa estrutura, você pode executar:</p>
<pre><code class="language-bash"># Compila tudo respeitando dependências
npm run build
Compila apenas core (rápido)
npm run build:core
Compila core e api
npm run build:api
Modo watch para desenvolvimento
npm run build:watch</code></pre>
<p>Quando você altera um arquivo em <code>core</code>, o <code>--watch</code> detecta a mudança, recompila <code>core</code>, e então automaticamente recompila <code>api</code> e <code>cli</code> (que dependem dele). Sem Project References, tudo seria recompilado do zero.</p>
<h2>Benefícios e Melhores Práticas</h2>
<p>Project References com monorepos oferece ganhos reais de performance, especialmente em projetos grandes. A compilação incremental reduz tempos de build de minutos para segundos em iterações normais de desenvolvimento. Além disso, Project References força uma separação clara de responsabilidades: cada projeto tem uma interface bem definida (seus <code>.d.ts</code>), prevenindo acoplamento indevido e dependências circulares.</p>
<p>Uma prática importante é evitar circular references. Se <code>core</code> referencia <code>api</code> e <code>api</code> referencia <code>core</code>, o TypeScript recusará compilar. Use uma arquitetura em camadas onde projetos de nível inferior não dependem de níveis superiores. Também é essencial configurar <code>skipLibCheck: true</code> na raiz para evitar verificação de tipo em <code>node_modules</code>, acelerando ainda mais o build.</p>
<p>Outra consideração é usar <code>--verbose</code> durante o desenvolvimento para entender o que está sendo compilado. Isso ajuda a identificar gargalos e garantir que seus Project References estão configurados corretamente. Se um projeto está sendo recompilado desnecessariamente, a configuração de referências pode estar incorreta.</p>
<h2>Conclusão</h2>
<p>Dominando Project References em TypeScript, você ganha a capacidade de construir monorepos escaláveis com builds incrementais eficientes. O primeiro aprendizado importante é que Project References não é apenas sintaxe — é uma mudança na forma como você estrutura e pensa sobre dependências entre partes do seu projeto. Segundo, builds incrementais com <code>--build</code> e <code>--watch</code> transformam a experiência de desenvolvimento, reduzindo tempos de espera e aumentando produtividade. Terceiro, uma arquitetura bem planejada com Project References força boas práticas de separação de responsabilidades, tornando o código mais testável e mantível a longo prazo.</p>
<h2>Referências</h2>
<ul>
<li><a href="https://www.typescriptlang.org/docs/handbook/project-references.html" target="_blank" rel="noopener noreferrer">TypeScript Project References - Documentação Oficial</a></li>
<li><a href="https://www.typescriptlang.org/tsconfig" target="_blank" rel="noopener noreferrer">TypeScript Handbook - tsconfig.json Compiler Options</a></li>
<li><a href="https://classic.yarnpkg.com/en/docs/workspaces/" target="_blank" rel="noopener noreferrer">Yarn Workspaces Documentation</a></li>
<li><a href="https://dev.to/shashkovdanil/monorepo-tools-comparison-3npe" target="_blank" rel="noopener noreferrer">Monorepo Tools Comparison - NX, Turborepo, Lerna</a></li>
<li><a href="https://pnpm.io/workspaces" target="_blank" rel="noopener noreferrer">pnpm Workspaces for Monorepo Management</a></li>
</ul>
<p><!-- FIM --></p>