Eine typsichere npm-Bibliothek von Grund auf bauen
AI generated
<T>
type
TypeScript · npm · Bibliotheken · Tooling
Eine typsichere npm-Bibliothek von Grund auf bauen
von der Projektstruktur bis zur öffentlichen API

Wer eine TypeScript-Bibliothek veröffentlicht, schreibt nicht einfach nur Code, sondern einen Vertrag mit fremden Projekten. Projektstruktur, tsconfig.json, Dual-Format-Build und das exports-Feld entscheiden darüber, ob eine TypeScript-Bibliothek in jedem Konsumenten-Setup reibungslos funktioniert oder täglich Supportanfragen erzeugt.

18 Min. Lesezeit tsconfig · tsup · exports-Feld · Semantic Versioning TypeScript 5.x · Node.js 20+

1. Warum eine TypeScript-Bibliothek andere Anforderungen hat als eine App

Eine Anwendung wird von einem einzigen Team in einer bekannten Umgebung gebaut und ausgeführt. Eine TypeScript-Bibliothek dagegen landet in Dutzenden fremden Projekten, mit unterschiedlichen Bundlern, unterschiedlichen tsconfig-Einstellungen und unterschiedlichen Node-Versionen. Ein Fehler in der eigenen App fällt sofort auf, ein Fehler in einer veröffentlichten TypeScript-Bibliothek erscheint erst Wochen später als Issue in einem fremden Repository, oft mit widersprüchlichen Fehlermeldungen, weil der Build des Konsumenten anders konfiguriert ist als der eigene.

Deshalb gelten für eine TypeScript-Bibliothek strengere Regeln als für internen Code: die öffentliche API muss stabil sein, die generierten Typdefinitionen müssen in strict-mode- und non-strict-mode-Projekten gleichermaßen funktionieren, und das Ausgabeformat muss sowohl mit ESM-only-Bundlern als auch mit klassischem CommonJS kompatibel sein. Wer diese Anforderungen von Anfang an mitdenkt, spart sich spätere Breaking Changes, die eine funktionierende TypeScript-Bibliothek nachträglich in Dutzenden Projekten kaputt machen.

2. Projektstruktur: src, dist und package.json von Anfang an sauber trennen

Die Struktur einer TypeScript-Bibliothek folgt einem einfachen Prinzip: Quellcode und Build-Artefakte dürfen sich niemals im selben Verzeichnis vermischen. Der Ordner src/ enthält ausschließlich handgeschriebenen TypeScript-Code, der Ordner dist/ enthält ausschließlich generierte Dateien und wird nie manuell bearbeitet oder eingecheckt. Diese Trennung verhindert, dass veraltete kompilierte Dateien versehentlich in Git landen oder dass ein Entwickler eine generierte .d.ts-Datei von Hand korrigiert, ohne zu merken, dass der nächste Build die Änderung wieder überschreibt.

Ein zweiter wichtiger Baustein ist die files-Angabe in der package.json, die genau festlegt, welche Verzeichnisse beim npm publish tatsächlich in das Tarball wandern. Ohne diese Angabe landen versehentlich Testdateien, Konfigurationsdateien oder sogar der komplette src/-Ordner im veröffentlichten Paket und blähen die installierte Größe unnötig auf. Eine gut strukturierte TypeScript-Bibliothek veröffentlicht ausschließlich dist/, die README und die Lizenzdatei.


{
  "name": "@mironsoft/query-builder",
  "version": "0.1.0",
  "description": "Type-safe query builder for relational databases",
  "type": "module",
  "main": "./dist/index.cjs",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "files": [
    "dist",
    "README.md",
    "LICENSE"
  ],
  "sideEffects": false,
  "engines": {
    "node": ">=18"
  },
  "scripts": {
    "build": "tsup src/index.ts --format esm,cjs --dts",
    "prepublishOnly": "npm run build"
  }
}

3. tsconfig.json für Bibliotheken: declaration, declarationMap, isolatedModules

Die tsconfig.json einer TypeScript-Bibliothek unterscheidet sich in entscheidenden Punkten von der einer Anwendung. Die Option declaration: true ist Pflicht, denn ohne sie erzeugt der Compiler keine .d.ts-Dateien und Konsumenten verlieren jede Typinformation. declarationMap: true ergänzt Declaration Maps, mit denen IDEs von der generierten Typdefinition direkt zum Original-Quellcode springen können, was die Fehlersuche in der eigenen Bibliothek erheblich erleichtert. isolatedModules: true stellt sicher, dass jede Datei unabhängig transpiliert werden kann, eine Voraussetzung für schnelle Build-Tools wie esbuild oder SWC, die Dateien parallel und ohne vollständige Typüberprüfung verarbeiten.

Ebenso wichtig ist skipLibCheck: true, weil eine TypeScript-Bibliothek die Typdefinitionen ihrer eigenen Abhängigkeiten nicht bei jedem Build erneut überprüfen sollte, das kostet nur Zeit und deckt selten eigene Fehler auf. declarationDir trennt die generierten Typdefinitionen bei Bedarf von den kompilierten JavaScript-Dateien, was in komplexeren Build-Pipelines mit mehreren Ausgabeformaten hilfreich ist. Die Kombination dieser Optionen bildet das Fundament, auf dem jede weitere Build-Entscheidung für eine TypeScript-Bibliothek aufbaut.


{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "declaration": true,
    "declarationMap": true,
    "isolatedModules": true,
    "skipLibCheck": true,
    "strict": true,
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "outDir": "dist",
    "rootDir": "src"
  },
  "include": ["src"],
  "exclude": ["dist", "**/*.test.ts", "examples"]
}

4. Dual-Format-Build: ESM und CJS gleichzeitig ausliefern

Ein häufiger Stolperstein bei einer TypeScript-Bibliothek ist die Annahme, dass alle Konsumenten längst auf ESM umgestiegen sind. In der Praxis existieren noch viele CommonJS-Projekte, ältere Jest-Konfigurationen ohne ESM-Unterstützung und Tools, die require() synchron aufrufen. Eine TypeScript-Bibliothek, die ausschließlich ESM ausliefert, schließt diese Konsumenten kategorisch aus oder zwingt sie zu komplizierten dynamischen Imports an Stellen, wo synchroner Code erwartet wird.

Der Dual-Format-Build löst dieses Problem, indem er aus derselben Quelle sowohl eine ESM-Datei (index.js) als auch eine CJS-Datei (index.cjs) erzeugt. Beide Varianten müssen inhaltlich identisch sein und dieselbe öffentliche API exportieren, sonst entstehen subtile Unterschiede zwischen dem Verhalten in ESM- und CJS-Kontext, die schwer zu reproduzieren sind. Für Namespace-Importe wie import * as lib from "..." braucht die CJS-Variante zusätzlich einen korrekten module.exports-Interop, den moderne Build-Tools automatisch erzeugen.

5. Build-Tooling: tsup, tsc und Rollup im Vergleich

Der native TypeScript-Compiler tsc kann zwar JavaScript und Deklarationen erzeugen, unterstützt aber keinen echten Dual-Format-Build in einem einzigen Durchlauf, sondern benötigt zwei separate tsconfig-Dateien und zwei Aufrufe. Für kleinere Bibliotheken ist das akzeptabel, für größere Projekte mit mehreren Entry-Points wird es schnell unübersichtlich. tsup, ein Wrapper um esbuild, erzeugt ESM und CJS in einem Befehl, bundelt Abhängigkeiten optional und generiert über einen zusätzlichen tsc-Aufruf im Hintergrund korrekte .d.ts-Dateien.

Rollup bleibt die richtige Wahl, wenn eine TypeScript-Bibliothek Plugins für komplexes Tree-Shaking, benutzerdefinierte Ausgabeformate oder Code-Splitting über mehrere Entry-Points benötigt, die tsup nicht abdeckt. Für die meisten Bibliotheken ohne exotische Anforderungen ist tsup jedoch der pragmatischste Einstieg: minimale Konfiguration, schnelle Build-Zeiten durch esbuild und automatische Unterstützung für Dual-Format-Ausgabe inklusive Typdefinitionen.


// tsup.config.ts — dual format build with type declarations
import { defineConfig } from "tsup";

export default defineConfig({
  entry: ["src/index.ts"],
  format: ["esm", "cjs"],
  dts: true,
  splitting: false,
  sourcemap: true,
  clean: true,
  minify: false,
  target: "es2020",
});

6. Das exports-Feld typsicher gestalten

Das exports-Feld in package.json ist der Mechanismus, mit dem Node.js und moderne Bundler entscheiden, welche Datei für welchen Import-Kontext geladen wird. Für eine TypeScript-Bibliothek ist entscheidend, dass innerhalb jedes Conditional Exports auch der passende types-Eintrag steht, und zwar an erster Stelle im jeweiligen Objekt. TypeScript liest das exports-Feld von oben nach unten und nimmt den ersten passenden Eintrag, ein falsch sortiertes Objekt führt dazu, dass Konsumenten die CJS-Typen bekommen, obwohl sie ESM importieren, oder umgekehrt.

Subpath-Exports erlauben es, Teile der TypeScript-Bibliothek gezielt freizugeben, etwa @mironsoft/query-builder/adapters/mysql, ohne den gesamten Adapter-Code in den Haupteinstiegspunkt zu bündeln. Das reduziert die Bundle-Größe für Konsumenten, die nur einen einzelnen Adapter benötigen. Wichtig ist, für jeden Subpath denselben dreiteiligen Aufbau aus types, import und require zu wiederholen, sonst bricht die Typauflösung genau für diesen Unterpfad.


{
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "import": "./dist/index.js",
      "require": "./dist/index.cjs"
    },
    "./adapters/mysql": {
      "types": "./dist/adapters/mysql.d.ts",
      "import": "./dist/adapters/mysql.js",
      "require": "./dist/adapters/mysql.cjs"
    },
    "./package.json": "./package.json"
  }
}

7. Die öffentliche API bewusst gestalten

Jede exportierte Funktion, Klasse oder jeder exportierte Typ einer TypeScript-Bibliothek ist ein Vertrag, der ab dem ersten Release nicht mehr ohne Breaking Change entfernt werden kann. Deshalb sollte der öffentliche Einstiegspunkt src/index.ts als bewusst kuratierte Barrel-Datei angelegt werden, die nur exportiert, was tatsächlich für Konsumenten gedacht ist. Interne Hilfsfunktionen, interne Typen und Implementierungsdetails bleiben in separaten Dateien ohne Re-Export über die Barrel-Datei.

Ein bewährtes Muster ist die klare Trennung zwischen öffentlichen und internen Modulen über die Ordnerstruktur: src/internal/ für Code, der niemals über den Haupteinstiegspunkt erreichbar sein soll, src/public/ oder direkt src/ für alles, was zur API gehört. Diese Disziplin verhindert, dass eine TypeScript-Bibliothek versehentlich Implementierungsdetails preisgibt, auf die sich Konsumenten dann verlassen, obwohl sie sich beim nächsten Refactoring ändern sollen.


// src/index.ts — deliberately curated public entry point
export { QueryBuilder } from "./query-builder";
export type { QueryBuilderOptions, Column, SortDirection } from "./types";

// Internal helpers stay in src/internal and are never re-exported here
// export * from "./internal/sql-escape"; — intentionally NOT exported

8. Versionierung und Semantic Versioning für Typänderungen

Semantic Versioning gilt für eine TypeScript-Bibliothek nicht nur für Laufzeitverhalten, sondern genauso für Typen. Eine Typänderung, die bestehenden Konsumenten-Code zum Kompilierfehler macht, ist ein Major-Release, selbst wenn sich am Laufzeitverhalten nichts ändert. Das Hinzufügen eines optionalen Parameters oder einer neuen exportierten Funktion ist ein Minor-Release. Die Korrektur eines fehlerhaften Typs, der vorher zu lax war, kann paradoxerweise ebenfalls ein Breaking Change sein, weil Code, der sich auf die zu lockere Typisierung verlassen hat, plötzlich nicht mehr kompiliert.

Für eine TypeScript-Bibliothek empfiehlt sich deshalb, jede Änderung an öffentlichen Typen vor dem Release testweise gegen ein kleines Beispielprojekt zu kompilieren, das repräsentative Konsumenten-Nutzungsmuster enthält. Tools wie Changesets automatisieren diesen Prozess, indem sie bei jedem Pull Request abfragen, ob es sich um ein Patch-, Minor- oder Major-Release handelt, und daraus automatisch den Changelog sowie die neue Versionsnummer ableiten.

9. Build-Strategien im direkten Vergleich

Die Wahl des Build-Tools und des Ausgabeformats hat direkten Einfluss darauf, wie zuverlässig eine TypeScript-Bibliothek in fremden Projekten funktioniert. Die folgende Übersicht vergleicht die gängigsten Ansätze.

Ansatz Aufwand Dual-Format Empfehlung
Nur tsc, ESM only Gering Nein Nur für interne Pakete geeignet
tsc mit zwei tsconfig-Dateien Mittel Ja Für sehr kleine Bibliotheken ausreichend
tsup Gering Ja Pragmatischer Standard für die meisten Bibliotheken
Rollup mit Plugins Hoch Ja Bei komplexem Tree-Shaking und Code-Splitting

Für die überwiegende Mehrheit der TypeScript-Bibliotheken ist tsup die richtige Balance zwischen Konfigurationsaufwand und Ergebnis. Rollup lohnt sich erst, wenn tatsächlich spezielle Anforderungen an Bundle-Splitting oder Ausgabeformate bestehen, die über einfaches ESM- und CJS-Bündeln hinausgehen.

Mironsoft

TypeScript-Bibliotheken, Tooling und Build-Pipelines

Eine eigene TypeScript-Bibliothek entwickeln lassen?

Wir konzipieren und bauen TypeScript-Bibliotheken mit sauberer öffentlicher API, Dual-Format-Build und stabiler Versionierung, damit euer Team und externe Konsumenten sich auf jedes Release verlassen können.

Projekt-Setup

tsconfig, Build-Tooling und exports-Feld von Grund auf sauber aufsetzen

API-Design

Öffentliche Schnittstelle bewusst gestalten und vor Breaking Changes schützen

Release-Pipeline

Semantic Versioning, Changesets und CI-gestützte Veröffentlichung einrichten

10. Zusammenfassung

Eine typsichere TypeScript-Bibliothek von Grund auf zu bauen bedeutet, von der ersten Zeile Code an in Verträgen zu denken statt in internen Implementierungsdetails. Die klare Trennung von src/ und dist/, eine tsconfig.json mit declaration, declarationMap und isolatedModules, ein Dual-Format-Build für ESM und CJS sowie ein korrekt sortiertes exports-Feld bilden das technische Fundament. Eine bewusst kuratierte öffentliche API und konsequentes Semantic Versioning sorgen dafür, dass Konsumenten der Bibliothek vertrauen können.

Wer diese Bausteine von Anfang an einplant, spart sich später aufwendige Migrationen und Breaking-Change-Kommunikation. Build-Tools wie tsup nehmen dabei einen großen Teil der Komplexität ab, ohne die Kontrolle über die generierten Typdefinitionen zu verlieren. Der Aufwand, eine TypeScript-Bibliothek von Anfang an richtig aufzusetzen, ist deutlich geringer als der Aufwand, eine bereits verbreitete Bibliothek nachträglich auf ein sauberes Fundament zu heben.

TypeScript-Bibliothek von Grund auf — Das Wichtigste auf einen Blick

Projektstruktur

src/ und dist/ strikt trennen, files in package.json begrenzt das veröffentlichte Tarball auf das Nötigste.

tsconfig.json

declaration, declarationMap und isolatedModules sind für Bibliotheken Pflicht, nicht optional.

Dual-Format-Build

ESM und CJS parallel ausliefern, mit tsup als pragmatischem Standard-Tool für die meisten Bibliotheken.

API & Versionierung

Öffentliche API bewusst kuratieren, exports-Feld korrekt sortieren, Semantic Versioning auch für Typänderungen anwenden.

11. FAQ: TypeScript-Bibliothek von Grund auf

1Brauche ich wirklich einen Dual-Format-Build?
Ja, sobald die Bibliothek öffentlich ist. CommonJS-Projekte und ältere Jest-Setups brauchen weiterhin CJS. tsup erzeugt beide Formate mit wenig Zusatzaufwand.
2Unterschied main, module, exports?
main ist klassisch CJS, module wird von manchen Bundlern für ESM gelesen, exports ist der moderne Standard mit Vorrang vor beiden.
3Warum src und dist trennen?
Damit generierte Dateien nicht in Git landen und niemand versehentlich eine kompilierte Datei statt der Quelle bearbeitet.
4Interne Funktionen vor Export schützen?
Über eine kuratierte Barrel-Datei als einzigen Einstiegspunkt, interner Code bleibt ohne Re-Export unerreichbar.
5Reicht tsc allein?
Für sehr einfache Fälle ja, aber ohne echten Dual-Format-Build in einem Durchlauf. tsup automatisiert das.
6Falsche Reihenfolge im exports-Objekt?
TypeScript nimmt den ersten Treffer. Steht types nicht zuerst, gibt es any-Typen oder Compile-Fehler beim Konsumenten.
7Zählt eine Typkorrektur als Breaking Change?
Ja, oft. Konsumenten-Code, der sich auf die zu lockere alte Typisierung verließ, kann danach nicht mehr kompilieren.
8Wie groß sollte der erste Release sein?
Möglichst klein. Ein schlanker Kern lässt sich risikofrei über Minor-Releases erweitern.
9Sollte sideEffects: false immer gesetzt sein?
Nur wenn die Bibliothek tatsächlich frei von Import-Seiteneffekten ist. Dann ermöglicht es effektives Tree-Shaking.
10Wie teste ich echte Konsumenten-Kompatibilität?
Mit npm pack und lokaler Installation in einem Testprojekt, das ESM, CJS, strict und non-strict tsconfig abdeckt.