Monorepos TypeScript en 2026: tsup, project references y paths sin dolor

Tienes tres paquetes en el mismo repositorio: @mi-empresa/ui, @mi-empresa/utils y @mi-empresa/api. La lógica de negocio está en utils, los componentes en ui, y la app en api usa ambos. Tiene todo el sentido del mundo tenerlos juntos, sin publicar nada a npm, compartiendo tipos y código al momento.

El problema llega cuando TypeScript no sabe dónde están esos paquetes. Sin configuración adicional, el compilador falla al resolver las importaciones entre paquetes, o los recompila enteros en cada build aunque no haya cambiado nada. El resultado: builds lentos, errores de tipos confusos y un flujo de trabajo que pide a gritos un poco de orden.

Hay tres caminos para solucionarlo: paths en tsconfig, project references, o delegar en un build tool como Turborepo. Cada uno tiene su sitio. Este artículo explica los tres sin rodeos.

tsconfig paths: el atajo que funciona para empezar

La opción más rápida es configurar paths en el tsconfig del paquete que consume a otros:

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@mi-empresa/utils": ["../utils/src/index.ts"],
      "@mi-empresa/ui": ["../ui/src/index.ts"]
    }
  }
}

Con esto, TypeScript resuelve las importaciones directamente a los archivos fuente. Sin compilar antes los paquetes dependientes, el type checker ya entiende los tipos. Para un monorepo pequeño donde no quieres complicarte, es suficiente.

El inconveniente es que hay que repetir esta configuración en cada paquete que importe a los demás, y los bundlers (Webpack, Vite, esbuild) necesitan su propia configuración para seguir los mismos alias, porque paths solo afecta a TypeScript, no al proceso de empaquetado. Si el monorepo crece, la configuración duplicada se vuelve un lastre.

Project references: el enfoque oficial

Las project references son la solución que TypeScript ofrece de forma nativa para este problema. La idea: declararas explícitamente qué paquetes dependen de cuáles, y el compilador aprovecha ese grafo para compilar solo lo que ha cambiado.

Cómo se configura

En el tsconfig del paquete consumidor, añades una sección references:

{
  "compilerOptions": {
    "outDir": "./dist"
  },
  "references": [
    { "path": "../utils" },
    { "path": "../ui" }
  ]
}

Y en el tsconfig del paquete referenciado (utils, ui...) necesitas activar composite: true:

{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "declarationMap": true,
    "outDir": "./dist"
  }
}

Con eso en su sitio, tsc --build compila el grafo completo en el orden correcto y guarda un archivo .tsbuildinfo por paquete. En la siguiente compilación, solo reconstruye lo que cambió. En monorepos grandes, la diferencia de velocidad es notable.

La pega es que hay más configuración que mantener, y cuando algo falla (un .tsbuildinfo desactualizado, una referencia circular) el error no siempre es fácil de interpretar. Para proyectos medianos y grandes merece la pena; para algo con dos o tres paquetes, puede ser demasiado.

tsup: compilar paquetes de librería sin quebraderos de cabeza

Si las project references te parecen verbosas para paquetes internos, tsup es la alternativa más práctica. Está construido sobre esbuild, es rápido y genera CJS y ESM simultáneamente con una sola orden:

tsup src/index.ts --format cjs,esm --dts

Eso produce dist/index.js (CommonJS), dist/index.mjs (ESM) y dist/index.d.ts (declaraciones de tipos). El package.json del paquete quedaría así:

{
  "main": "./dist/index.js",
  "module": "./dist/index.mjs",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "require": "./dist/index.js",
      "import": "./dist/index.mjs",
      "types": "./dist/index.d.ts"
    }
  }
}

Para la mayoría de paquetes internos de un monorepo, tsup cubre el noventa por ciento de los casos con muy poca configuración. Puedes definir todo en un tsup.config.ts si necesitas algo más específico: múltiples entradas, external dependencies, plugins de esbuild, etc.

El campo exports en package.json

Desde Node.js 12 y con cualquier bundler moderno, el campo exports define exactamente cómo se importa un paquete. Sustituye (y supera) al clásico main. La ventaja principal es que puedes exponer sub-paths:

{
  "exports": {
    ".": {
      "import": "./dist/esm/index.js",
      "require": "./dist/cjs/index.js",
      "types": "./dist/index.d.ts"
    },
    "./utils": {
      "import": "./dist/esm/utils.js",
      "require": "./dist/cjs/utils.js",
      "types": "./dist/utils.d.ts"
    }
  }
}

Con esto, alguien puede hacer import { algo } from '@mi-empresa/utils/utils' y obtener solo esa parte del paquete, sin importar el índice completo.

Para que TypeScript respete el campo exports, necesitas "moduleResolution": "bundler" o "node16" en tus opciones de compilador. Con "moduleResolution": "node" (el clásico) TypeScript ignora exports y busca los archivos directamente.

Turborepo: orquestar builds sin perder la cabeza

Cuando tienes varios paquetes con dependencias entre ellos, el orden de compilación importa. Turborepo resuelve exactamente eso: lee el grafo de dependencias de tus package.json y ejecuta las tareas en el orden correcto, en paralelo siempre que puede.

turbo run build

Esa orden compila todos los paquetes del monorepo, respetando que ui necesita que utils esté compilado primero. El archivo turbo.json define el pipeline:

{
  "$schema": "https://turbo.build/schema.json",
  "pipeline": {
    "build": {
      "dependsOn": ["^build"],
      "outputs": ["dist/**"]
    },
    "dev": {
      "cache": false,
      "persistent": true
    }
  }
}

El ^build significa "ejecuta primero el build de todas mis dependencias". Simple y directo.

Lo que marca la diferencia respecto a ejecutar scripts manualmente es la caché. Turborepo guarda el hash del código fuente y los outputs de cada tarea. Si no cambió nada en utils, la siguiente vez que ejecutes turbo run build, ese paquete no se recompila: se restaura desde caché en milisegundos. Con la caché remota de Vercel, ese ahorro se comparte entre todos los miembros del equipo y el CI.

isolatedDeclarations: tipos rápidos en TypeScript 5.5+

TypeScript 5.5 introdujo isolatedDeclarations. Con esta opción activada, las declaraciones de tipos (.d.ts) se pueden generar sin ejecutar el type checker completo, lo que permite a herramientas como esbuild producirlas mucho más rápido.

{
  "compilerOptions": {
    "isolatedDeclarations": true
  }
}

La restricción es que tus exports tienen que estar anotados explícitamente. Si exportas una función y TypeScript necesita inferir su tipo de retorno a partir de lógica compleja, isolatedDeclarations falla. Tienes que poner la anotación tú:

// Sin anotación: falla con isolatedDeclarations
export function calcular(a: number, b: number) {
  return a + b;
}

// Con anotación: OK
export function calcular(a: number, b: number): number {
  return a + b;
}

Para paquetes de librería con una API pública bien definida, esta restricción raramente es un problema. La ganancia en velocidad de build puede ser significativa en monorepos grandes.

Workspace protocols: cómo declarar dependencias locales

Para que el gestor de paquetes entienda que @mi-empresa/utils es un paquete local del monorepo y no algo de npm, cada herramienta tiene su sintaxis.

pnpm workspaces

En el package.json raíz, configuras los workspaces:

{
  "name": "mi-monorepo",
  "private": true,
  "workspaces": ["packages/*", "apps/*"]
}

Y en el package.json de cada app o paquete que consuma otro, usas el protocolo workspace::

{
  "dependencies": {
    "@mi-empresa/utils": "workspace:*",
    "@mi-empresa/ui": "workspace:*"
  }
}

El workspace:* le dice a pnpm que use la versión local, la que tienes en el monorepo, sin mirar npm.

npm workspaces

npm soporta workspaces desde la versión 7. La configuración en el raíz es igual:

{
  "workspaces": ["packages/*"]
}

Pero npm no tiene un protocolo workspace: como pnpm. Puedes referenciar paquetes locales con "*" y npm los resuelve automáticamente desde el workspace.

Yarn Berry

Yarn Berry (v2+) usa el mismo protocolo workspace:* que pnpm y añade PnP (Plug'n'Play) para resolver módulos sin node_modules. La compatibilidad con PnP varía según las herramientas, así que en proyectos nuevos conviene comprobar que tsup y Turborepo funcionan bien antes de comprometerse.

El combo que más se usa en 2026

Si empiezas un monorepo TypeScript hoy, la combinación más extendida es pnpm workspaces + Turborepo + tsup. pnpm gestiona las dependencias con eficiencia (un solo node_modules en el raíz con symlinks), Turborepo orquesta los builds y la caché, y tsup compila cada paquete a CJS y ESM con sus tipos.

Las project references tienen su sitio en proyectos donde necesitas compilación incremental dentro de un mismo paquete grande, o cuando no quieres introducir un build tool externo. Los paths en tsconfig siguen siendo útiles para desarrollo local cuando no necesitas outputs compilados.

Si quieres profundizar en cómo ha evolucionado TypeScript y sus herramientas de build, puedes echar un vistazo a TypeScript y las herramientas de build en 2025. Y si trabajas en proyectos a gran escala, el artículo sobre JavaScript en proyectos a gran escala complementa bien este tema.

Imagen: Pexels / Antonio Batini?

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP