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