TypeScript

Dominando Project References em TypeScript: Monorepos e Builds Incrementais em Projetos Reais

18 min de leitura

Dominando Project References em TypeScript: Monorepos e Builds Incrementais em Projetos Reais

Project References: A Estrutura Fundamental do TypeScript 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 . Diferente de módulos tradicionais, Project References estabelece uma relação de dependência explícita 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. 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 (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. Como Project References Funciona O mecanismo é simples: você declara referências de projeto no usando a propriedade . Quando o TypeScript vê uma referência,

<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 &quot;workspaces&quot;), 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 &quot;módulo virtual&quot; 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">{

&quot;compilerOptions&quot;: {

&quot;target&quot;: &quot;ES2020&quot;,

&quot;module&quot;: &quot;ESNext&quot;,

&quot;lib&quot;: [&quot;ES2020&quot;],

&quot;moduleResolution&quot;: &quot;node&quot;,

&quot;strict&quot;: true,

&quot;esModuleInterop&quot;: true,

&quot;skipLibCheck&quot;: true,

&quot;forceConsistentCasingInFileNames&quot;: true,

&quot;declaration&quot;: true,

&quot;declarationMap&quot;: true,

&quot;sourceMap&quot;: true,

&quot;outDir&quot;: &quot;../dist&quot;

},

&quot;include&quot;: [],

&quot;references&quot;: []

}</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">{

&quot;extends&quot;: &quot;../../tsconfig.json&quot;,

&quot;compilerOptions&quot;: {

&quot;outDir&quot;: &quot;../../dist/core&quot;

},

&quot;include&quot;: [&quot;src&quot;],

&quot;references&quot;: []

}</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">{

&quot;extends&quot;: &quot;../../tsconfig.json&quot;,

&quot;compilerOptions&quot;: {

&quot;outDir&quot;: &quot;../../dist/api&quot;

},

&quot;include&quot;: [&quot;src&quot;],

&quot;references&quot;: [

{ &quot;path&quot;: &quot;../core&quot; }

]

}</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">{

&quot;scripts&quot;: {

&quot;build&quot;: &quot;tsc --build packages/core packages/api packages/cli&quot;,

&quot;build:watch&quot;: &quot;tsc --build --watch packages/core packages/api packages/cli&quot;,

&quot;build:clean&quot;: &quot;tsc --build --clean packages/core packages/api packages/cli&quot;,

&quot;build:core&quot;: &quot;tsc --build packages/core&quot;

}

}</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(&quot;Division by zero&quot;);

return a / b;

}</code></pre>

<p><strong>packages/core/src/logger.ts:</strong></p>

<pre><code class="language-typescript">export interface LoggerOptions {

level: &quot;info&quot; | &quot;warn&quot; | &quot;error&quot;;

timestamp: boolean;

}

export class Logger {

private options: LoggerOptions;

constructor(options: LoggerOptions = { level: &quot;info&quot;, timestamp: true }) {

this.options = options;

}

log(message: string): void {

const prefix = this.options.timestamp ? [${new Date().toISOString()}] : &quot;&quot;;

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 &quot;./math&quot;;

export * from &quot;./logger&quot;;</code></pre>

<p><strong>packages/core/tsconfig.json:</strong></p>

<pre><code class="language-json">{

&quot;extends&quot;: &quot;../../tsconfig.json&quot;,

&quot;compilerOptions&quot;: {

&quot;outDir&quot;: &quot;../../dist/core&quot;

},

&quot;include&quot;: [&quot;src&quot;],

&quot;references&quot;: []

}</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 &quot;@monorepo/core&quot;;

const logger = new Logger({ level: &quot;info&quot;, timestamp: true });

export interface Request {

operation: &quot;add&quot; | &quot;multiply&quot;;

a: number;

b: number;

}

export function handleRequest(req: Request): number {

logger.log(Processing ${req.operation} operation);

switch (req.operation) {

case &quot;add&quot;:

return add(req.a, req.b);

case &quot;multiply&quot;:

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 &quot;./server&quot;;

const result = handleRequest({ operation: &quot;add&quot;, a: 10, b: 5 });

console.log(&quot;Result:&quot;, result);</code></pre>

<p><strong>packages/api/tsconfig.json:</strong></p>

<pre><code class="language-json">{

&quot;extends&quot;: &quot;../../tsconfig.json&quot;,

&quot;compilerOptions&quot;: {

&quot;outDir&quot;: &quot;../../dist/api&quot;

},

&quot;include&quot;: [&quot;src&quot;],

&quot;references&quot;: [

{ &quot;path&quot;: &quot;../core&quot; }

]

}</code></pre>

<p><strong>packages/api/package.json:</strong></p>

<pre><code class="language-json">{

&quot;name&quot;: &quot;@monorepo/api&quot;,

&quot;version&quot;: &quot;1.0.0&quot;,

&quot;dependencies&quot;: {

&quot;@monorepo/core&quot;: &quot;workspace:*&quot;

}

}</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 &quot;@monorepo/core&quot;;

const logger = new Logger({ level: &quot;info&quot;, timestamp: true });

export function executeCalculation(args: string[]): void {

if (args.length &lt; 3) {

logger.log(&quot;Usage: cli &lt;operation&gt; &lt;a&gt; &lt;b&gt;&quot;);

return;

}

const [operation, aStr, bStr] = args;

const a = parseFloat(aStr);

const b = parseFloat(bStr);

if (isNaN(a) || isNaN(b)) {

logger.log(&quot;Invalid numbers provided&quot;);

return;

}

try {

if (operation === &quot;divide&quot;) {

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 &quot;./commands&quot;;

const args = process.argv.slice(2);

executeCalculation(args);</code></pre>

<p><strong>packages/cli/tsconfig.json:</strong></p>

<pre><code class="language-json">{

&quot;extends&quot;: &quot;../../tsconfig.json&quot;,

&quot;compilerOptions&quot;: {

&quot;outDir&quot;: &quot;../../dist/cli&quot;

},

&quot;include&quot;: [&quot;src&quot;],

&quot;references&quot;: [

{ &quot;path&quot;: &quot;../core&quot; }

]

}</code></pre>

<h3>Configuração Raiz do Monorepo</h3>

<p><strong>tsconfig.json (raiz):</strong></p>

<pre><code class="language-json">{

&quot;compilerOptions&quot;: {

&quot;target&quot;: &quot;ES2020&quot;,

&quot;module&quot;: &quot;commonjs&quot;,

&quot;lib&quot;: [&quot;ES2020&quot;],

&quot;moduleResolution&quot;: &quot;node&quot;,

&quot;strict&quot;: true,

&quot;esModuleInterop&quot;: true,

&quot;skipLibCheck&quot;: true,

&quot;forceConsistentCasingInFileNames&quot;: true,

&quot;declaration&quot;: true,

&quot;declarationMap&quot;: true,

&quot;sourceMap&quot;: true,

&quot;baseUrl&quot;: &quot;.&quot;,

&quot;paths&quot;: {

&quot;@monorepo/core&quot;: [&quot;packages/core/src&quot;],

&quot;@monorepo/api&quot;: [&quot;packages/api/src&quot;],

&quot;@monorepo/cli&quot;: [&quot;packages/cli/src&quot;]

}

},

&quot;include&quot;: [],

&quot;references&quot;: [

{ &quot;path&quot;: &quot;packages/core&quot; },

{ &quot;path&quot;: &quot;packages/api&quot; },

{ &quot;path&quot;: &quot;packages/cli&quot; }

]

}</code></pre>

<p><strong>package.json (raiz):</strong></p>

<pre><code class="language-json">{

&quot;name&quot;: &quot;monorepo&quot;,

&quot;version&quot;: &quot;1.0.0&quot;,

&quot;workspaces&quot;: [

&quot;packages/*&quot;

],

&quot;scripts&quot;: {

&quot;build&quot;: &quot;tsc --build&quot;,

&quot;build:watch&quot;: &quot;tsc --build --watch&quot;,

&quot;build:clean&quot;: &quot;tsc --build --clean&quot;,

&quot;build:core&quot;: &quot;tsc --build packages/core&quot;,

&quot;build:api&quot;: &quot;tsc --build packages/core packages/api&quot;,

&quot;build:cli&quot;: &quot;tsc --build packages/core packages/cli&quot;

},

&quot;devDependencies&quot;: {

&quot;typescript&quot;: &quot;^5.3.0&quot;

}

}</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>&lt;!-- FIM --&gt;</p>

Comentários

Mais em TypeScript

Testes de Integração com TypeScript: Banco Real e Fixtures Tipadas: Do Básico ao Avançado
Testes de Integração com TypeScript: Banco Real e Fixtures Tipadas: Do Básico ao Avançado

O que são Testes de Integração e Por Que Importam Testes de integração valida...

Como Usar Type Aliases em TypeScript: Diferenças Práticas em Relação a Interfaces em Produção
Como Usar Type Aliases em TypeScript: Diferenças Práticas em Relação a Interfaces em Produção

O Que São Type Aliases e Interfaces em TypeScript? Type aliases e interfaces...

O que Todo Dev Deve Saber sobre Herança e Polimorfismo em TypeScript com Classes e Interfaces
O que Todo Dev Deve Saber sobre Herança e Polimorfismo em TypeScript com Classes e Interfaces

Entendendo Herança em TypeScript A herança é um dos pilares da Programação Or...