TypeScript Project References für schnelle Monorepo-Builds
AI generated
type
TypeScript
Project References
für TypeScript-Monorepos, die nicht bei jedem Build neu kompilieren

composite, references und tsc --build ersetzen den vollständigen Neu-Compile durch inkrementelle, parallelisierbare Teil-Builds pro Paket.

10 Min. Lesezeit TypeScript 5.x Monorepo

1. Das Problem: langsame tsc-Builds in Monorepos

Ohne Project References behandelt ein einzelner tsc-Lauf über ein Monorepo alle Pakete als eine große Kompilationseinheit. Jede Änderung, auch in einem einzelnen Blatt-Paket, zwingt den Compiler dazu, den gesamten Typgraphen erneut zu prüfen.

Bei mittelgroßen Monorepos mit einem Dutzend Paketen bedeutet das oft Kompilierzeiten im zweistelligen Sekundenbereich für eine Ein-Zeilen-Änderung, was den Watch-Modus in der IDE spürbar träge macht und den Feedback-Loop beim Entwickeln unterbricht.

Project References lösen dieses Problem strukturell: jedes Paket wird zu einer eigenen Kompilationseinheit mit klar definierten Abhängigkeiten, sodass der Compiler nur die tatsächlich betroffenen Pakete neu bauen muss, nicht das gesamte Repository.

Ohne diese Struktur wachsen Kompilierzeiten in der Praxis überproportional mit der Anzahl der Pakete, weil jede neue Datei den gesamten Typgraphen weiter aufbläht. Mit wachsendem Team und wachsendem Repository wird dieser Effekt zunehmend zum größten Reibungspunkt im täglichen Entwickeln.

2. composite und references in tsconfig.json

Ein Paket wird referenzierbar, indem composite: true in seiner tsconfig gesetzt wird. Das erzwingt zusätzliche Regeln: alle Eingabedateien müssen explizit vom include-Muster erfasst sein, und declaration wird implizit aktiviert, weil abhängige Pakete die generierten .d.ts-Dateien statt des Quellcodes konsumieren.

Im konsumierenden Paket verweist das references-Array auf die Pfade der abhängigen Pakete. TypeScript liest daraus einen gerichteten azyklischen Graphen und weiß dadurch genau, in welcher Reihenfolge Pakete gebaut werden müssen.

Wichtig ist, dass references nur die direkte Build-Reihenfolge steuert, nicht die Modulauflösung selbst. Für echte Importe zwischen Paketen sind weiterhin Workspace-Verlinkungen über npm, pnpm oder Yarn nötig, Project References ergänzen diese, ersetzen sie aber nicht.


// packages/core/tsconfig.json
{
  "compilerOptions": {
    "composite": true,
    "declaration": true,
    "declarationMap": true,
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src"]
}

// packages/api/tsconfig.json
{
  "compilerOptions": { "composite": true, "outDir": "dist", "rootDir": "src" },
  "references": [{ "path": "../core" }],
  "include": ["src"]
}

3. Build-Reihenfolge und der .tsbuildinfo-Cache

Bei jedem Build eines referenzierten Pakets schreibt TypeScript eine .tsbuildinfo-Datei, die Dateihashes, Diagnosen und Abhängigkeitsinformationen enthält. Beim nächsten Build vergleicht der Compiler diesen Zustand und kompiliert nur, was sich seit dem letzten Lauf tatsächlich geändert hat.

Dieser Cache ist der eigentliche Grund für die Geschwindigkeit: ein Build ohne Änderungen dauert dank .tsbuildinfo nur Millisekunden, weil der Compiler früh erkennt, dass kein Paket im Graphen neu kompiliert werden muss.

In CI-Umgebungen lohnt es sich, die .tsbuildinfo-Dateien zwischen Runs zu cachen, etwa über den Build-Cache von GitHub Actions oder GitLab CI, weil ein kalter Cache den Geschwindigkeitsvorteil für den ersten Build wieder zunichtemacht.

4. tsc --build: inkrementell und mit --build parallelisierbar

Der Befehl tsc --build (kurz tsc -b) liest den Referenzgraphen ausgehend von der angegebenen tsconfig und baut alle Abhängigkeiten in korrekter Reihenfolge, bevor das Zielpaket selbst gebaut wird.

Mit dem Flag --watch kombiniert entsteht ein Multi-Package-Watch-Modus, der nach einer Änderung in einem Kernpaket automatisch nur die davon abhängigen Pakete neu baut, anstatt den kompletten Watch-Prozess neu zu starten.

Zusätzliche Flags wie --verbose zeigen an, welche Pakete übersprungen wurden und warum, was beim Debuggen von unerwartet langsamen Builds oder falsch konfigurierten Referenzen unverzichtbar ist.


# Baut alle referenzierten Pakete in korrekter Reihenfolge
tsc --build packages/api

# Watch-Modus über den gesamten Referenzgraphen
tsc --build --watch

# Erzwingt einen vollständigen Rebuild, ignoriert .tsbuildinfo
tsc --build --force

5. declarationMap und Cross-Package Go-to-Definition

Ohne declarationMap: true führt Go to Definition auf ein Symbol aus einem referenzierten Paket lediglich in die generierte .d.ts-Datei, nicht in den eigentlichen Quellcode. Für tägliche Entwicklungsarbeit ist das unpraktisch, weil Kommentare und die tatsächliche Implementierung fehlen.

Mit aktiviertem declarationMap generiert TypeScript zusätzliche Sourcemap-Dateien für die Deklarationen, sodass der Editor bei Go-to-Definition direkt in die .ts-Quelldatei des referenzierten Pakets springt, inklusive aller Kommentare und JSDoc-Blöcke.

Dieses Feature ist besonders in großen Monorepos ein spürbarer Produktivitätsgewinn, weil Entwickler beim Navigieren zwischen Paketgrenzen nicht mehr merken, dass sie überhaupt eine Paketgrenze überschritten haben.

6. Path Mapping vs. Project References: der Unterschied

Viele Projekte nutzen paths in der tsconfig für kürzere Importe wie @core/utils. Das ist reines Alias-Mapping für die Modulauflösung und hat keinerlei Einfluss auf Build-Reihenfolge oder inkrementelle Kompilierung.

Project References lösen ein anderes Problem: sie definieren, welche Kompilationseinheiten existieren und in welcher Reihenfolge sie gebaut werden müssen. Beide Mechanismen schließen sich nicht aus, sondern werden in der Praxis meist kombiniert.

Ein häufiger Fehler ist die Annahme, paths allein würde Cross-Package-Typsicherheit garantieren. Ohne composite und references prüfen sich Pakete gegenseitig nicht konsistent, und Änderungen an einem Kernpaket färben nicht zuverlässig auf abhängige Pakete ab.

7. Typische Fallstricke: zirkuläre Referenzen, outDir-Konflikte

Zirkuläre Referenzen zwischen zwei Paketen führt TypeScript mit einer klaren Fehlermeldung ab, weil der Referenzgraph azyklisch sein muss. In der Praxis deutet ein solcher Zyklus fast immer auf ein Architekturproblem hin, das eine Extraktion eines gemeinsamen Basis-Pakets erfordert.

Ein weiterer häufiger Fehler ist ein falsch konfiguriertes outDir, das versehentlich Quelldateien anderer Pakete überschreibt, wenn rootDir nicht sauber pro Paket gesetzt ist. Die Fehlermeldung dazu ist oft kryptisch und zeigt sich erst als unerwartet leere oder überschriebene Ausgabedatei.

Auch das Vergessen von declaration: true in einem referenzierten Paket ist ein Klassiker: ohne generierte .d.ts-Dateien können abhängige Pakete das Paket zwar im Dateisystem finden, aber keine sinnvollen Typen daraus importieren.

8. Integration mit Turborepo und Nx

Build-Orchestratoren wie Turborepo oder Nx bauen eine eigene Aufgaben-Graph-Logik oberhalb des Paketmanagers, die sich mit Project References ergänzt statt konkurriert: Project References sorgen für korrekte TypeScript-Builds pro Paket, Turborepo oder Nx orchestrieren, cachen und parallelisieren diese Aufgaben über das gesamte Repository.

In der Praxis ruft eine Turborepo-Pipeline pro Paket typischerweise tsc --build auf und cached die Ausgabe inklusive .tsbuildinfo, sodass sowohl der TypeScript-eigene inkrementelle Cache als auch der Remote-Cache des Orchestrators greifen.

Wichtig ist dabei, die .tsbuildinfo-Dateien als Build-Ausgabe im Orchestrator zu deklarieren, sonst wird bei einem Cache-Hit zwar der kompilierte Output wiederhergestellt, nicht aber der Zustand, den tsc --build für seine eigene Inkrementalität benötigt.

9. Migrationsstrategie für bestehende Monorepos

Eine schrittweise Migration beginnt an den Blättern des Abhängigkeitsgraphen: Pakete ohne interne Abhängigkeiten erhalten zuerst composite: true, danach folgen Pakete, die nur bereits migrierte Pakete referenzieren.

Ein realistischer Zeitrahmen für ein Repository mit einem Dutzend Paketen liegt bei wenigen Arbeitstagen, wobei der größte Aufwand meist nicht im Setup selbst liegt, sondern im Aufräumen historisch gewachsener, unsauberer Paketgrenzen, die vorher nie explizit gemacht werden mussten.

Die folgende Tabelle vergleicht die wichtigsten Ansätze für Monorepo-Builds und hilft bei der Entscheidung, wann sich der Migrationsaufwand lohnt.

Ansatz Inkrementell Cross-Package-Typsicherheit Setup-Aufwand
Ein tsconfig fürs ganze Repo Nein Ja, aber langsam Minimal
Path Mapping ohne References Nein Nur oberflächlich Gering
Project References + tsc -b Ja, über .tsbuildinfo Ja, strukturell erzwungen Mittel
Project References + Turborepo/Nx Ja, plus Remote-Cache Ja Hoch, aber skaliert

Mironsoft

TypeScript-Migration, Typsicherheit und Team-Onboarding

JavaScript-Codebasis ohne Typsicherheit, aber keine Zeit für eine Rundum-Migration?

Wir migrieren bestehende JavaScript-Projekte schrittweise zu TypeScript, richten strikte Compiler-Einstellungen sauber ein und bringen Teams mit Code-Reviews und Style-Guides auf denselben Typsicherheits-Stand.

Migrations-Fahrplan

Schrittweise JS-zu-TS-Migration ohne Big-Bang-Risiko planen und umsetzen.

Strict-Mode-Einführung

tsconfig.json, ESLint-Regeln und CI-Checks für dauerhafte Typsicherheit aufsetzen.

Team-Onboarding

Entwickler mit Workshops und Code-Reviews in TypeScript-Best-Practices einarbeiten.

10. Zusammenfassung

TS Project References

Kernmechanismus

composite + references teilen ein Repo in eigenständige Kompilationseinheiten.

Geschwindigkeit

.tsbuildinfo macht unveränderte Pakete zu Millisekunden-Builds.

IDE-Komfort

declarationMap bringt Go-to-Definition zurück in den Quellcode.

Ergänzung

Turborepo/Nx orchestrieren, Project References sichern Korrektheit.

11. FAQ: TS Project References

1Muss ich alle Pakete gleichzeitig auf composite umstellen?
Nein, die Migration kann schrittweise von den Blättern des Abhängigkeitsgraphen aus erfolgen. Ein Paket ohne composite kann weiterhin normal referenziert werden, solange es selbst keine Referenzen benötigt.
2Was passiert, wenn ich .tsbuildinfo versehentlich lösche?
Der nächste Build ist ein vollständiger Neu-Build, weil TypeScript keinen vorherigen Zustand zum Vergleichen findet. Funktional ändert sich nichts, es kostet nur einmalig Zeit.
3Kann ich Project References ohne den tsc-Compiler direkt, etwa mit esbuild, nutzen?
esbuild ignoriert Project References vollständig, da es keine Typprüfung durchführt. Project References sind ein Feature des TypeScript-Compilers selbst und relevant für Typprüfung und .d.ts-Generierung, nicht für reine Transpilation.
4Sollte .tsbuildinfo ins Git-Repository eingecheckt werden?
Üblicherweise nicht, die Datei gehört in .gitignore und wird stattdessen im CI-Cache oder lokal generiert. Ein eingechecktes .tsbuildinfo kann bei abweichenden Umgebungen zu widersprüchlichen Zuständen führen.
5Erzwingt composite automatisch strict-Modus?
Nein, composite und strict sind unabhängige Optionen. composite erzwingt lediglich explizite Includes, aktivierte declaration und ein definiertes outDir, unabhängig von den Strictness-Einstellungen.
6Wie gehe ich mit einer zirkulären Abhängigkeit zwischen zwei Paketen um?
TypeScript verweigert eine solche Referenz mit einer Fehlermeldung. Die übliche Lösung ist, den gemeinsamen, gegenseitig benötigten Code in ein drittes, gemeinsames Basis-Paket zu extrahieren, das beide referenzieren.
7Funktioniert tsc --build --watch performant auch bei zwanzig oder mehr Paketen?
Ja, weil der Watch-Modus dank .tsbuildinfo pro Paket entscheidet, ob ein Rebuild nötig ist. Die Größe des Repos wirkt sich vor allem auf den allerersten kalten Build aus, nicht auf inkrementelle Änderungen.
8Ersetzt Project References einen Bundler wie Vite oder Webpack?
Nein, Project References betreffen ausschließlich Typprüfung und .d.ts-Generierung durch tsc. Für das Bundling der Laufzeit-JavaScript-Dateien wird weiterhin ein separater Bundler benötigt.
9Was ist der Unterschied zwischen tsc --build und einem normalen tsc-Aufruf im referenzierten Paket?
Ein normaler tsc-Aufruf ignoriert das references-Array für die Build-Orchestrierung und prüft nur das aktuelle Paket. tsc --build liest den gesamten Referenzgraphen und baut Abhängigkeiten in korrekter Reihenfolge mit.
10Lohnt sich Project References auch für ein kleines Monorepo mit drei Paketen?
Der Geschwindigkeitsgewinn ist bei so wenigen Paketen meist gering, aber die strukturelle Klarheit über Paketgrenzen und die verbesserte IDE-Navigation können den geringen Setup-Aufwand trotzdem rechtfertigen.