O projeto começa com um tsconfig.json na raiz. Depois surgem uma biblioteca compartilhada, uma API, um app web e testes que não deveriam entrar no mesmo build. O compilador continua vendo uma pasta grande, o editor abre arquivos de projetos diferentes e o CI roda um comando que não representa a estrutura real.

Project References resolvem essa fronteira. Você transforma cada parte em um projeto TypeScript compilável, declara as dependências no tsconfig.json e usa tsc -b para descobrir a ordem. O recurso não é um substituto para npm workspaces nem uma justificativa para reorganizar qualquer repositório pequeno.

Este guia começa com um monorepo mínimo e termina com os critérios para decidir se a configuração vale a manutenção. O código é ilustrativo, não foi executado em um projeto de produção. Os comandos de verificação mostram como testar o mesmo arranjo na sua máquina.

Diagrama mostra um tsconfig único se dividindo em projetos core e api ligados por tsc -b.

Resposta curta

  • Use Project References quando seu repositório tiver projetos que precisam de fronteiras, builds separados ou uma ordem de dependências explícita.
  • Ative composite nos projetos referenciados, gere declarações e mantenha um tsconfig de solução com files: [].
  • Rode tsc -b na raiz. tsc sem --build não executa o grafo de referências da mesma forma.
  • Se você tem poucos arquivos e nenhum limite real entre projetos, vários tsconfig podem adicionar mais trabalho que valor.

O que você precisa antes de começar?

Você precisa de Node.js em uma versão LTS mantida, npm, um terminal e familiaridade básica com package.json e tsconfig.json. O exemplo usa npm workspaces, mas a ideia do grafo também vale para pnpm, Yarn ou diretórios que não são publicados como pacotes. Instale o TypeScript no workspace antes de executar o script:

npm install --save-dev typescript @types/node

O objetivo é terminar com dois projetos compiláveis, um diretório dist por projeto e uma configuração de solução que você consegue executar a partir da raiz. Se o seu repositório usa um bundler, mantenha o comando de typecheck separado do comando que empacota JavaScript.

Quando Project References valem a configuração?

O TypeScript introduziu Project References na versão 3.0 para dividir programas em partes menores e trabalhar com o modo --build (TypeScript, “Project References”, consultado em 24/08/2026). O recurso vale a configuração quando a divisão representa uma dependência real, não apenas uma preferência por pastas menores.

Procure estes sinais:

  • uma biblioteca precisa ser compilada antes de uma API que a consome;
  • testes e código de produção têm entradas, saídas ou opções diferentes;
  • o editor e o CI precisam enxergar os mesmos limites;
  • uma mudança em um pacote não deveria reprocessar todos os outros;
  • a equipe quer tratar declarações .d.ts como a fronteira pública de um projeto.

Não leia a promessa de “build mais rápido” como um benchmark. A documentação oficial descreve o mecanismo de verificar quais projetos estão atualizados e compilar os que precisam, mas o ganho depende da forma do grafo, do bundler e da quantidade de código. Meça o seu repositório depois de a configuração estar correta.

A pergunta mais útil não é “quantos pacotes existem?”. É “qual projeto deveria ser capaz de compilar sem abrir o código interno do outro?”. Se a resposta for clara para pelo menos uma fronteira, Project References pode expressar uma decisão de arquitetura. Se ninguém consegue nomear a fronteira, o problema ainda é de organização, não de tsconfig.

O que npm workspaces resolve e o que não resolve?

O npm define workspaces como recursos para gerenciar vários pacotes locais a partir de um pacote raiz. O npm install cria os links locais sem exigir npm link manual (npm, “Workspaces”, consultado em 24/08/2026). Isso resolve a instalação e a resolução de pacotes do workspace. Não descreve a ordem de compilação do TypeScript.

Pense nas responsabilidades como duas camadas:

Camada Pergunta Recurso
Pacotes Como @acme/core aparece no node_modules local? npm workspaces
Compilação O que precisa ser construído antes de apps/api? Project References
Tipos Qual saída representa a fronteira de core? declaration e .d.ts
Orquestração Qual comando descobre a ordem e pula o que está atualizado? tsc -b

Um workspace pode linkar core para api e ainda deixar o compilador tratando os dois como uma única lista de arquivos. O inverso também é possível: Project References podem organizar diretórios que não usam npm workspaces. Usar os dois é comum porque um resolve a relação entre pacotes e o outro resolve a relação entre projetos TypeScript.

Se o artigo sobre como criar um servidor MCP em TypeScript para agentes de código é o projeto que você está começando, uma única configuração provavelmente basta. Quando o servidor passa a compartilhar código com outro pacote, a pergunta muda para a fronteira de compilação.

Como montar um monorepo TypeScript mínimo?

Comece com uma biblioteca e uma aplicação. Essa forma pequena deixa o grafo visível e evita introduzir React, um bundler ou um gerenciador de monorepo antes de entender o compilador.

.
├── package.json
├── tsconfig.json
├── packages/
│   └── core/
│       ├── package.json
│       ├── tsconfig.json
│       └── src/index.ts
└── apps/
    └── api/
        ├── package.json
        ├── tsconfig.json
        └── src/index.ts

No package.json da raiz, declare os workspaces e os scripts que deixam o comando de build explícito:

{
  "name": "acme-workspace",
  "private": true,
  "workspaces": ["packages/*", "apps/*"],
  "scripts": {
    "build": "tsc -b tsconfig.json",
    "build:verbose": "tsc -b tsconfig.json --verbose"
  }
}

No pacote core, configure a saída que o consumidor vai ler:

{
  "name": "@acme/core",
  "version": "1.0.0",
  "main": "dist/index.js",
  "types": "dist/index.d.ts"
}

O package.json da API pode depender do nome do workspace:

{
  "name": "@acme/api",
  "version": "1.0.0",
  "dependencies": {
    "@acme/core": "1.0.0"
  }
}

Crie também uma função pequena em packages/core/src/index.ts para que a API tenha uma superfície pública concreta:

export function formatServiceName(service: string): string {
  return service.toUpperCase();
}

O npm documenta que uma dependência entre workspaces é vinculada localmente quando o pacote está definido na configuração do workspace (npm, “Adding dependencies to a workspace”, consultado em 24/08/2026). O link local torna o pacote encontrável. A referência no tsconfig ainda será necessária para o tsc -b conhecer a ordem.

Como declarar as referências e executar tsc -b?

Um projeto referenciado precisa de composite: true, e a raiz pode funcionar como uma solução sem arquivos próprios. A documentação do TypeScript também recomenda uma configuração com files: [] que aponta para os projetos folha (TypeScript, “Project References”, consultado em 24/08/2026).

Crie primeiro a configuração de solução:

{
  "files": [],
  "references": [
    { "path": "./packages/core" },
    { "path": "./apps/api" }
  ]
}

O core precisa declarar sua própria entrada, saída e declarações:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "composite": true,
    "declaration": true,
    "declarationMap": true,
    "rootDir": "src",
    "outDir": "dist"
  },
  "include": ["src/**/*.ts"]
}

No apps/api/tsconfig.json, repita as opções que precisam ser consistentes e adicione a dependência de projeto:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "strict": true,
    "composite": true,
    "declaration": true,
    "declarationMap": true,
    "rootDir": "src",
    "outDir": "dist"
  },
  "include": ["src/**/*.ts"],
  "references": [
    { "path": "../../packages/core" }
  ]
}

O src/index.ts da API pode importar o pacote pelo nome:

import { formatServiceName } from "@acme/core";

console.log(formatServiceName("billing"));

Depois de instalar as dependências e garantir que o TypeScript está disponível, rode:

npm install
npx tsc -b tsconfig.json --verbose

O modo --build encontra projetos referenciados, verifica o que está atualizado e constrói as dependências na ordem correta. O comando também pode receber o caminho de um projeto específico, como npx tsc -b apps/api.

O exemplo é intencionalmente pequeno. Em um projeto real, centralize as opções com extends, mas deixe rootDir, outDir e os arquivos incluídos coerentes em cada pacote. Um tsconfig.base.json compartilhado não deve esconder a direção das dependências.

O que composite, declaration e declarationMap mudam?

composite impõe restrições que ajudam o compilador a descobrir rapidamente se um projeto já foi construído. A referência do TypeScript explica que os arquivos de implementação precisam estar cobertos por include ou files, o rootDir tem um comportamento padrão diferente e declaration é ativado para o projeto referenciado (TypeScript, “Project References”, consultado em 24/08/2026).

Essas opções não são decoração. Elas tornam a saída do projeto parte do contrato:

  • composite diz que o projeto pode participar de um grafo de build;
  • declaration produz .d.ts para o consumidor enxergar a superfície tipada;
  • declarationMap liga a declaração ao código-fonte para recursos como Go to Definition;
  • outDir separa a saída de cada projeto e evita colisões;
  • tsBuildInfoFile, gerado por configurações incrementais, registra informação para builds seguintes.

O TSConfig Reference sobre declarationMap recomenda considerar essa opção quando você usa Project References. Ainda assim, ela não corrige um grafo errado. Se api referencia core, mas o código importa arquivos internos por um alias que ignora a fronteira do pacote, a declaração produzida não vai resolver o problema arquitetural.

Há um custo prático. Depois de clonar o repositório, os .d.ts esperados podem ainda não existir. A própria documentação alerta que você pode precisar compilar o projeto ou manter certas saídas versionadas para o editor navegar sem erros falsos. Decida isso como parte do fluxo de desenvolvimento, não depois do primeiro erro no computador de outra pessoa.

Não há um teste de produção neste artigo para escolher entre versionar dist e gerá-lo no clone. Essa decisão depende do bundler, do ambiente e da política do repositório. O ponto verificável é outro: o clone precisa conseguir gerar as declarações antes de uma ferramenta que depende delas tentar resolver o pacote.

Como verificar se o grafo está correto?

Uma configuração só está pronta quando o comando de build prova a relação que o arquivo declara. Rode a construção detalhada e leia quais projetos foram considerados atualizados, reconstruídos ou ignorados:

npx tsc -b tsconfig.json --verbose --pretty false

Para simular a decisão sem escrever as saídas, use --dry quando a versão do TypeScript instalada oferecer essa opção:

npx tsc -b tsconfig.json --dry --verbose

Depois da primeira execução, verifique os sinais que importam:

  1. packages/core/dist/index.d.ts existe e representa os exports públicos.
  2. apps/api compila sem importar arquivos de packages/core/src por caminho relativo.
  3. Alterar apenas core faz o build identificar api como dependente quando a saída pública muda.
  4. Remover uma entrada de references produz um erro que explica a dependência ausente.
  5. O mesmo script da raiz é usado no CI, em vez de um tsc --noEmit que carrega apenas um projeto.

Se o pacote usa um bundler, separe as funções. O bundler pode produzir JavaScript para a aplicação, enquanto tsc -b valida os limites e as declarações. Não apresente um build do bundler como prova de que o grafo de Project References está correto.

Para uma biblioteca consumida por outros pacotes, também vale revisar como você publica tipos. O artigo sobre servir definições TypeScript para o Eden Treaty trata de uma fronteira de pacote publicada. Aqui o foco é a fronteira interna, mas os dois problemas se encontram quando o .d.ts local vira parte do contrato que outro consumidor importa.

Por que o editor e o CI podem discordar?

O editor pode abrir um arquivo usando o tsconfig mais próximo, enquanto o CI executa um comando na raiz. Project References ajudam a tornar o grafo explícito, mas não fazem todas as ferramentas obedecerem ao mesmo programa. A documentação do TypeScript alerta que os projetos dependentes usam declarações construídas e que o tsc só constrói dependências automaticamente quando recebe --build (TypeScript, “Caveats for Project References”, consultado em 24/08/2026).

Os sintomas mais comuns são:

  • o editor navega para um tipo, mas tsc --noEmit não encontra o pacote;
  • a aplicação funciona depois de um build manual, mas falha em um clone limpo;
  • o lint de tipos lê fontes enquanto o compilador lê .d.ts produzidos;
  • um alias de paths aponta para src e ignora o pacote que o build publica;
  • o projeto foi referenciado, mas não tem composite, include ou outDir compatível.

O issue sobre tipos que viram any em pacotes com Project References é um bom registro de diagnóstico para esse tipo de atrito. Ele não é uma regra para todos os projetos. Use-o para lembrar que editor, linter, workspace e compilador podem resolver fronteiras de maneira diferente.

Quando uma ferramenta não entende o grafo, escolha uma estratégia explícita: configure essa ferramenta para ler os projetos corretos, rode o build antes da análise que depende de declarações ou mantenha uma configuração de typecheck separada. Não elimine as referências apenas para fazer um comando isolado passar sem entender o que ele estava verificando.

Quando não usar Project References?

Não use Project References apenas porque o repositório tem duas pastas. Se existe uma aplicação pequena, um único processo de build e nenhuma fronteira que a equipe queira proteger, um tsconfig.json simples pode ser mais legível.

Adie a adoção quando:

  • o bundler ainda não sabe consumir as saídas ou os aliases do novo arranjo;
  • os pacotes não têm uma relação de dependência que alguém consiga explicar;
  • o time não tem um comando de clone limpo que gere as declarações necessárias;
  • a única motivação é copiar uma configuração de um monorepo muito maior;
  • o problema real é um módulo mal organizado, não a duração ou a abrangência do typecheck.

Use Project References quando a fronteira for útil mesmo sem o ganho de tempo. Uma biblioteca que não deveria importar a aplicação, um conjunto de testes com configuração própria ou um pacote publicado internamente já pode justificar o grafo. O build incremental é uma consequência possível, não a única razão.

Se a arquitetura ainda estiver confusa, comece por separar serviços e responsabilidades no código. O guia sobre organizar serviços em uma arquitetura MSC com TypeScript trata dessa fronteira em outro nível. Project References tornam uma divisão existente verificável; não inventam uma boa divisão sozinhas.

Perguntas frequentes

Project References funcionam sem um monorepo?

Sim. A documentação do TypeScript descreve referências entre diretórios que têm seus próprios tsconfig.json. Um monorepo com workspaces é uma combinação comum, mas não é requisito. O que importa é haver projetos TypeScript separados e uma dependência que você quer declarar.

Posso usar tsc -p em vez de tsc -b?

Pode usar tsc -p para compilar um projeto isolado, mas esse comando não é o orquestrador do grafo. Para construir dependências na ordem e reaproveitar o estado conhecido pelo build, use tsc -b com a configuração de solução. Compare os comandos com --verbose antes de decidir o script do CI.

Preciso versionar os arquivos .d.ts e dist?

Não existe uma resposta universal. O TypeScript alerta que os projetos dependentes precisam das declarações para navegação e compilação. Você pode gerá-las após o clone, versioná-las ou fazer o ambiente de desenvolvimento cuidar desse passo. Escolha uma política e faça o CI detectar um clone sem as saídas.

Project References substituem Turborepo, Nx ou outro build tool?

Não. Eles descrevem o grafo que o TypeScript entende. Um build tool pode cuidar de cache, tarefas, bundling, testes e execução entre pacotes. Comece pelo recurso nativo quando o problema for a fronteira de compilação. Adicione outra camada quando existir uma necessidade que tsc -b não cobre.

Conclusão

Project References são uma forma de dizer ao TypeScript que o repositório tem projetos, não apenas pastas. Workspaces ligam os pacotes locais. references declara quem depende de quem. composite e declarações tornam a fronteira compilável. tsc -b prova a ordem e encontra o que precisa ser reconstruído.

Comece com duas partes e um grafo pequeno. Rode o build em um clone limpo, confira os .d.ts, alinhe o editor e o CI e só então divida mais projetos. Se a fronteira não explica uma decisão real do código, a configuração ainda não resolveu o problema que motivou a busca.

Fontes consultadas