von der .d.ts direkt zurück zum Original-Code
Wer Strg-Klick auf einen importierten Typ macht und in einer generierten .d.ts-Datei landet statt im eigenen Quellcode, verliert wertvolle Zeit beim Debugging. Declaration Maps schließen genau diese Lücke und machen aus einer TypeScript-Bibliothek eine Bibliothek, die sich in der IDE anfühlt, als läge der Quellcode direkt im eigenen Projekt.
Inhaltsverzeichnis
- 1. Das Problem: Strg-Klick landet in der generierten .d.ts
- 2. Was Declaration Maps technisch sind
- 3. declarationMap in tsconfig.json aktivieren
- 4. Zusammenspiel von declarationMap und sourceMap
- 5. Wie IDEs Declaration Maps tatsächlich nutzen
- 6. Declaration Maps beim Bibliotheks-Publishing
- 7. Stolpersteine: relative Pfade und verschobene Build-Ordner
- 8. Monorepo-Szenario: Declaration Maps über Package-Grenzen hinweg
- 9. Mit und ohne Declaration Maps im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Das Problem: Strg-Klick landet in der generierten .d.ts
Wer eine TypeScript-Bibliothek in einem fremden Projekt einsetzt und per Strg-Klick oder Cmd-Klick zur Definition eines importierten Typs springt, landet standardmäßig in der generierten .d.ts-Datei im dist/-Ordner der Bibliothek. Diese Datei enthält reine Typsignaturen ohne Implementierung, ohne Kommentare zum Kontext und ohne die ursprüngliche Struktur des Quellcodes. Für einfache Nachschlagevorgänge reicht das, für tiefere Recherche in der eigenen Bibliothek oder beim Debuggen eines Type-Level-Problems ist es unzureichend.
Besonders unangenehm wird es, wenn Entwickler an der eigenen TypeScript-Bibliothek arbeiten und zwischen mehreren Paketen eines Monorepos wechseln. Ohne Declaration Maps springt die IDE bei jedem Import eines internen Pakets in die kompilierte .d.ts, obwohl der Quellcode nur einen Ordner entfernt liegt. Das kostet bei jedem einzelnen Navigationsschritt Zeit und unterbricht den Denkfluss, gerade in großen Codebasen mit vielen internen Abhängigkeiten.
2. Was Declaration Maps technisch sind
Declaration Maps funktionieren nach demselben Prinzip wie klassische JavaScript-Source-Maps, nur eine Ebene höher: Statt kompiliertes JavaScript auf den ursprünglichen TypeScript-Quellcode abzubilden, bilden sie generierte .d.ts-Dateien auf den .ts-Quellcode ab, aus dem sie erzeugt wurden. Für jede index.d.ts erzeugt der Compiler eine begleitende index.d.ts.map-Datei, die Positionsinformationen im JSON-Format enthält und über einen Kommentar am Ende der .d.ts-Datei referenziert wird.
Diese Zuordnung erlaubt es IDEs, beim Klick auf "Go to Definition" nicht bei der Typsignatur stehen zu bleiben, sondern automatisch einen weiteren Sprung zur eigentlichen Implementierung im Quellcode zu machen. Das Ergebnis: Entwickler navigieren durch eine TypeScript-Bibliothek genauso, wie sie durch den eigenen Anwendungscode navigieren würden, komplett transparent, ohne den Unterschied zwischen kompiliertem Paket und Quellcode überhaupt wahrzunehmen.
3. declarationMap in tsconfig.json aktivieren
Die Aktivierung von Declaration Maps erfordert nur eine einzige zusätzliche Option in der tsconfig.json, hat aber eine Voraussetzung, die häufig übersehen wird: declaration: true muss ebenfalls gesetzt sein, denn ohne generierte .d.ts-Dateien gibt es nichts, worauf eine Declaration Map verweisen könnte. Zusätzlich muss sourceMap: true aktiv sein, damit die Kette von der Declaration Map bis zur eigentlichen .ts-Quelldatei vollständig geschlossen ist.
Ein wichtiger Praxis-Hinweis: declarationMap ohne declaration erzeugt einen Compiler-Fehler, TypeScript verweigert dann den Build komplett. Wer diese Option in einem bestehenden Projekt aktivieren möchte, sollte deshalb zuerst prüfen, ob declaration bereits gesetzt ist, bevor die neue Option ergänzt wird.
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"strict": true
},
"include": ["src"]
}
4. Zusammenspiel von declarationMap und sourceMap
Declaration Maps und klassische Source Maps lösen zwei unterschiedliche Probleme, die häufig verwechselt werden. Eine klassische Source Map bildet kompiliertes JavaScript zur Laufzeit auf TypeScript-Quellcode ab, damit Stack-Traces und Debugger-Breakpoints im Original-Code funktionieren. Eine Declaration Map dagegen bildet die statische Typinformation in der .d.ts-Datei auf den Quellcode ab, damit die IDE zur Entwicklungszeit navigieren kann. Beide Mechanismen arbeiten unabhängig voneinander, ergänzen sich aber, wenn eine TypeScript-Bibliothek sowohl zur Laufzeit debuggbar als auch zur Entwicklungszeit navigierbar sein soll.
In der generierten index.d.ts-Datei erscheint am Ende ein Kommentar wie //# sourceMappingURL=index.d.ts.map, der auf die begleitende Map-Datei verweist. Diese Map-Datei enthält wiederum einen sources-Eintrag, der relativ zur Map-Datei auf die ursprüngliche .ts-Datei zeigt. Wird eine dieser relativen Pfadangaben durch einen fehlerhaften Build-Prozess verschoben, bricht die Verkettung, und die IDE fällt stillschweigend zurück auf die reine .d.ts-Navigation, ohne eine Fehlermeldung anzuzeigen.
// dist/index.d.ts.map — generated declaration map (simplified)
{
"version": 3,
"file": "index.d.ts",
"sourceRoot": "",
"sources": ["../src/index.ts"],
"names": [],
"mappings": "AAAA,cAAc,EAAE,MAAM,WAAW,CAAC"
}
5. Wie IDEs Declaration Maps tatsächlich nutzen
Visual Studio Code unterstützt Declaration Maps ohne zusätzliche Konfiguration, sofern die Einstellung typescript.preferGoToSourceDefinition aktiviert oder die Standardnavigation genutzt wird. Beim ersten Sprung landet der Cursor in der .d.ts-Datei, ein zweiter Aufruf von "Go to Definition" oder die alternative Aktion "Go to Source Definition" folgt dann der Declaration Map bis zum eigentlichen Quellcode. Dieses zweistufige Verhalten ist beabsichtigt, weil manche Entwickler bewusst die öffentliche Typsignatur sehen wollen, bevor sie tiefer in die Implementierung springen.
JetBrains-IDEs wie WebStorm und PhpStorm mit dem TypeScript-Plugin lösen Declaration Maps automatisch beim ersten Navigationsversuch auf, ohne einen Zwischenschritt über die .d.ts-Datei zu erzwingen. Voraussetzung ist in beiden IDE-Familien, dass die Map-Dateien tatsächlich im veröffentlichten Paket vorhanden sind und die referenzierten Quelldateien für die IDE erreichbar sind, entweder über node_modules mit ausgelieferten Quellen oder über ein lokal verlinktes Monorepo-Paket.
// .vscode/settings.json — prefer jumping straight to source, skip the .d.ts hop
{
"typescript.preferGoToSourceDefinition": true,
"typescript.tsdk": "node_modules/typescript/lib"
}
6. Declaration Maps beim Bibliotheks-Publishing
Eine häufig übersehene Voraussetzung: Declaration Maps funktionieren bei extern installierten Paketen nur, wenn die ursprünglichen .ts-Quelldateien tatsächlich mit im npm-Paket enthalten sind. Wird in der files-Angabe der package.json nur dist/ veröffentlicht, verweist die Declaration Map zwar korrekt auf ../src/index.ts, aber diese Datei existiert im installierten node_modules-Verzeichnis schlicht nicht. Die IDE kann dann nicht navigieren und fällt auf die .d.ts-Ansicht zurück.
Für eine TypeScript-Bibliothek, die von der vollen Declaration-Map-Erfahrung profitieren soll, muss deshalb auch der src/-Ordner im veröffentlichten Paket enthalten sein. Der zusätzliche Platzbedarf ist in den meisten Fällen gering, weil TypeScript-Quelldateien typischerweise kleiner sind als die kompilierten Ausgaben mit zusätzlichen Kommentaren und Formatierung. Wer die Paketgröße dennoch minimal halten will, kann testweise mit npm pack --dry-run prüfen, welche Dateien tatsächlich im Tarball landen.
# Verify that src/ is actually part of the published tarball
npm pack --dry-run
# Example output should list both dist/ and src/
# npm notice ???? @mironsoft/query-builder@1.2.0
# npm notice === Tarball Contents ===
# npm notice 1.1kB dist/index.d.ts
# npm notice 0.3kB dist/index.d.ts.map
# npm notice 2.4kB dist/index.js
# npm notice 1.8kB src/index.ts
7. Stolpersteine: relative Pfade und verschobene Build-Ordner
Der häufigste Fehler bei Declaration Maps entsteht, wenn rootDir und outDir in der tsconfig.json nicht konsistent zur tatsächlichen Ordnerstruktur stehen. Wird beispielsweise ein Build-Skript verwendet, das die Ausgabe nachträglich in einen anderen Ordner verschiebt, ohne die Map-Dateien entsprechend anzupassen, zeigen die relativen Pfade in der .d.ts.map ins Leere. Die IDE zeigt dann keinen Fehler, sondern navigiert einfach nicht weiter, was das Problem schwer diagnostizierbar macht.
Ein zweiter Stolperstein betrifft Build-Tools, die Deklarationen nachträglich mit einem separaten Bundler für .d.ts-Dateien zusammenfassen, etwa um mehrere kleine .d.ts-Dateien in eine einzige Rollup-Datei zu verschmelzen. Diese Tools müssen explizit Declaration-Map-Unterstützung mitbringen, sonst gehen die Zuordnungen beim Zusammenführen verloren. Vor dem Einsatz eines solchen Tools lohnt sich ein Blick in dessen Dokumentation, ob declarationMap überhaupt unterstützt wird.
8. Monorepo-Szenario: Declaration Maps über Package-Grenzen hinweg
In einem Monorepo mit mehreren internen TypeScript-Paketen zeigen Declaration Maps ihren größten Nutzen. Über TypeScript Project References mit composite: true und aktivierten Declaration Maps navigiert die IDE beim Import eines internen Pakets direkt in den Quellcode des jeweiligen Pakets, statt in dessen kompilierten dist/-Ordner. Voraussetzung ist, dass jedes Projekt im Monorepo composite: true zusätzlich zu declarationMap: true gesetzt hat, denn Project References verlangen composite Projects als Grundvoraussetzung.
Für Teams, die täglich zwischen mehreren Paketen eines Monorepos wechseln, ist dieser Effekt spürbar: Ein Bugfix in einem gemeinsam genutzten Utility-Paket lässt sich direkt aus dem aufrufenden Paket heraus finden und bearbeiten, ohne manuell zwischen Ordnern zu wechseln oder eine Dateisuche zu starten. Declaration Maps machen ein Monorepo in der Praxis so nutzbar, wie es sich anfühlen sollte: als ein einziges, zusammenhängendes Projekt statt als Sammlung isolierter Pakete.
// packages/utils/tsconfig.json — required for cross-package navigation
{
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true,
"outDir": "dist",
"rootDir": "src"
}
}
9. Mit und ohne Declaration Maps im Vergleich
Der Unterschied zwischen einer TypeScript-Bibliothek mit und ohne Declaration Maps zeigt sich nicht im Laufzeitverhalten, sondern ausschließlich in der täglichen Entwicklererfahrung. Die folgende Tabelle fasst die wichtigsten Unterschiede zusammen.
| Aspekt | Ohne Declaration Maps | Mit Declaration Maps |
|---|---|---|
| Go to Definition | Landet in generierter .d.ts | Springt bis zum Original-Quellcode |
| Kommentare im Quellcode | Nicht sichtbar | Vollständig sichtbar im Ursprung |
| Monorepo-Navigation | Über dist/ pro Paket | Direkt zwischen src/-Ordnern |
| Paketgröße | Minimal | Etwas größer durch src/ und .map |
Der geringe Mehraufwand an Paketgröße steht in keinem Verhältnis zum Zeitgewinn bei der täglichen Navigation, besonders für Teams, die eine TypeScript-Bibliothek intensiv weiterentwickeln oder in einem Monorepo mit vielen internen Abhängigkeiten arbeiten.
Eine weitere Beobachtung aus der Praxis: Sobald ein Team einmal an Declaration Maps gewöhnt ist, fällt deren Fehlen in einem anderen Projekt sofort negativ auf. Der gefühlte Rückschritt bei der Navigation motiviert häufig genau die Teams, die zuvor skeptisch gegenüber dem zusätzlichen Konfigurationsaufwand waren, die Option nachträglich in weiteren internen Paketen zu aktivieren.
Mironsoft
TypeScript-Tooling, Monorepos und Entwicklererfahrung
TypeScript-Monorepo mit besserer Navigation aufsetzen?
Wir richten Declaration Maps, Project References und Build-Pipelines so ein, dass euer Team zwischen internen Paketen navigiert, als wäre alles ein einziges Projekt.
tsconfig-Audit
Bestehende Konfiguration auf fehlende Declaration Maps und Project References prüfen
Monorepo-Setup
Composite Projects und Package-übergreifende Navigation einrichten
Publishing-Check
Paket-Tarball prüfen, damit Declaration Maps auch bei Konsumenten funktionieren
10. Zusammenfassung
Declaration Maps lösen ein konkretes, alltägliches Problem: die Navigation von einer generierten .d.ts-Datei zurück zum eigentlichen Quellcode einer TypeScript-Bibliothek. Mit declarationMap: true, declaration: true und sourceMap: true in der tsconfig.json ist die Aktivierung in wenigen Zeilen erledigt. Beim Publishing muss zusätzlich der src/-Ordner mit ausgeliefert werden, sonst läuft die Zuordnung bei externen Konsumenten ins Leere.
Der größte praktische Effekt zeigt sich in Monorepos mit vielen internen Paketen, wo Declaration Maps zusammen mit Project References eine nahtlose Navigation über Package-Grenzen hinweg ermöglichen. Der geringe zusätzliche Aufwand bei Konfiguration und Paketgröße rechtfertigt sich durch die spürbar bessere Entwicklererfahrung, besonders in Teams, die täglich mit der eigenen TypeScript-Bibliothek arbeiten.
Declaration Maps — Das Wichtigste auf einen Blick
Aktivierung
declaration, declarationMap und sourceMap gemeinsam in der tsconfig.json setzen.
Publishing
Den src/-Ordner mit veröffentlichen, sonst laufen die Map-Pfade bei Konsumenten ins Leere.
IDE-Verhalten
VS Code und JetBrains-IDEs folgen der Kette von .d.ts.map bis zur .ts-Quelldatei automatisch.
Monorepo
Zusammen mit composite: true und Project References die beste Navigationserfahrung über Pakete hinweg.