ohne gebündelte Typen und ohne Duplicate-Identifier-Fehler
Eine TypeScript-Bibliothek, die React, Vue oder eine andere Wirtsbibliothek erweitert, darf deren Typen nicht selbst bündeln, sonst kollidieren zwei Kopien derselben Typdefinition beim Konsumenten. Peer Dependencies typsicher zu deklarieren bedeutet, den richtigen Versionsbereich, die richtigen Imports und die richtige CI-Testmatrix für genau dieses Problem aufzusetzen.
Inhaltsverzeichnis
- 1. Was Peer Dependencies sind und wann man sie braucht
- 2. peerDependencies vs. dependencies vs. devDependencies
- 3. peerDependenciesMeta und optionale Peers
- 4. Typen für Peer Dependencies bereitstellen ohne sie zu bündeln
- 5. Versionsbereiche für Peer Types richtig festlegen
- 6. Testing-Matrix: Peer-Versionen in CI absichern
- 7. Type-Only Imports für Peer Packages
- 8. Häufige Fehler: doppelte Typdefinitionen und Duplicate Identifier
- 9. Peer-Dependency-Strategien im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Was Peer Dependencies sind und wann man sie braucht
Eine Peer Dependency ist eine Abhängigkeit, die eine Bibliothek zwar zwingend benötigt, aber nicht selbst mitbringen soll, weil der Konsument diese Abhängigkeit ohnehin schon in seinem eigenen Projekt installiert hat. Das klassische Beispiel ist ein React-Hook-Paket oder ein Vue-Plugin: Beide erwarten, dass React beziehungsweise Vue bereits vom Hauptprojekt bereitgestellt wird, und würden bei einer eigenen, zusätzlichen Installation zu zwei unterschiedlichen React-Instanzen im selben Bundle führen, was zur Laufzeit zu Fehlern wie "Invalid Hook Call" führt.
Für eine TypeScript-Bibliothek kommt eine zusätzliche Dimension hinzu: nicht nur die Laufzeitbibliothek selbst, sondern auch deren Typdefinitionen dürfen nicht doppelt im Projekt landen. Peer Dependencies sind deshalb immer dann die richtige Wahl, wenn eine Bibliothek eng an eine Wirtsbibliothek gekoppelt ist, deren Typen erweitert oder deren Instanzen geteilt werden müssen, etwa bei Plugin-Systemen, Framework-Integrationen oder Adaptern für Build-Tools.
2. peerDependencies vs. dependencies vs. devDependencies
Die Wahl der richtigen Abhängigkeitskategorie in package.json entscheidet darüber, ob eine TypeScript-Bibliothek beim Konsumenten sauber funktioniert oder zu Versionskonflikten führt. dependencies sind Pakete, die jede Installation zwingend mitbringt, unabhängig davon, was der Konsument bereits installiert hat, das ist richtig für Hilfsbibliotheken, die niemand geteilt haben muss. peerDependencies signalisieren dagegen: "Diese Version wird benötigt, aber du, der Konsument, musst sie selbst bereitstellen." devDependencies werden nur für die Entwicklung der Bibliothek selbst gebraucht und landen nie im veröffentlichten Paket.
Ein häufiger Fehler ist, eine Wirtsbibliothek versehentlich als reguläre dependency statt als peerDependency einzutragen. Das Ergebnis: npm installiert eine zweite Kopie von React oder Vue in node_modules, TypeScript sieht plötzlich zwei unterschiedliche @types/react-Pakete und meldet Typkonflikte, obwohl der Code auf den ersten Blick korrekt aussieht. Die korrekte Deklaration in package.json ist deshalb der erste und wichtigste Schritt.
{
"name": "@mironsoft/react-form-toolkit",
"version": "1.0.0",
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
},
"devDependencies": {
"react": "^18.3.0",
"react-dom": "^18.3.0",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0"
}
}
3. peerDependenciesMeta und optionale Peers
Nicht jede Peer Dependency wird von allen Konsumenten benötigt. Eine TypeScript-Bibliothek, die sowohl React als auch Vue über separate Adapter unterstützt, will niemanden zwingen, beide Frameworks zu installieren. Genau dafür existiert peerDependenciesMeta: mit dem Feld optional: true wird eine Peer Dependency als nicht zwingend markiert, npm gibt dann keine Warnung mehr aus, wenn diese Abhängigkeit fehlt.
Wichtig ist, dass peerDependenciesMeta ausschließlich das Verhalten des Package-Managers steuert, nicht das von TypeScript selbst. Der eigene Code muss zusätzlich mit Laufzeit-Checks oder bedingten Imports darauf reagieren, dass eine optionale Peer Dependency fehlen könnte, sonst crasht die Bibliothek trotz korrekt konfigurierter package.json beim ersten Zugriff auf das fehlende Paket.
{
"peerDependencies": {
"react": ">=18.0.0",
"vue": ">=3.3.0"
},
"peerDependenciesMeta": {
"react": { "optional": true },
"vue": { "optional": true }
}
}
4. Typen für Peer Dependencies bereitstellen ohne sie zu bündeln
Der entscheidende Grundsatz beim Umgang mit Typen für Peer Dependencies lautet: niemals die @types-Pakete der Wirtsbibliothek in dependencies eintragen, sondern ausschließlich in devDependencies. Zur Entwicklungszeit der eigenen TypeScript-Bibliothek werden diese Typen gebraucht, um den eigenen Code gegen die korrekten Signaturen zu kompilieren, aber sie dürfen niemals Teil des veröffentlichten Pakets werden, weil der Konsument seine eigene, möglicherweise neuere Version der Typen mitbringt.
Landen die @types-Pakete versehentlich als reguläre Abhängigkeit im veröffentlichten Paket, installiert npm zwei Versionen derselben Typdefinitionen, TypeScript erkennt sie als unterschiedliche, nominell inkompatible Typen und meldet Fehler wie "Type 'ReactNode' is not assignable to type 'ReactNode'", obwohl der Name identisch ist. Dieser verwirrende Fehler ist praktisch immer ein Symptom für falsch deklarierte Peer-Type-Abhängigkeiten.
5. Versionsbereiche für Peer Types richtig festlegen
Der Versionsbereich in peerDependencies sollte so breit wie möglich und so eng wie nötig sein. Eine zu enge Angabe wie "react": "18.2.0" zwingt jeden Konsumenten auf exakt diese Version, was in der Praxis fast nie funktioniert, weil Projekte selten exakt dieselbe Patch-Version einer großen Bibliothek installiert haben. Der bewährte Ansatz ist ein Bereich über mehrere Major-Versionen, sofern die eigene TypeScript-Bibliothek tatsächlich mit allen getestet wurde, etwa "react": "^17.0.0 || ^18.0.0 || ^19.0.0".
Für die Typen selbst gilt eine zusätzliche Regel: die eigene Bibliothek sollte sich in devDependencies auf die niedrigste unterstützte Version der Peer Dependency und ihrer Typen festlegen, nicht auf die neueste. So wird sichergestellt, dass die eigene Bibliothek tatsächlich mit der ältesten deklarierten Version kompiliert und nicht versehentlich API-Oberflächen einer neueren Version nutzt, die in der deklarierten Minimalversion noch gar nicht existieren.
{
"peerDependencies": {
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"devDependencies": {
"react": "17.0.2",
"@types/react": "17.0.80"
}
}
6. Testing-Matrix: Peer-Versionen in CI absichern
Ein breiter Versionsbereich in peerDependencies ist nur ein Versprechen, kein Beweis. Ohne automatisierte Tests gegen mehrere tatsächliche Peer-Versionen weiß niemand, ob die TypeScript-Bibliothek wirklich mit React 17 genauso funktioniert wie mit React 19. Eine CI-Matrix, die den Build und die Tests gegen jede unterstützte Major-Version der Peer Dependency wiederholt, deckt Kompatibilitätsprobleme auf, bevor Konsumenten sie melden.
Besonders wichtig ist, in dieser Matrix nicht nur die Laufzeitbibliothek, sondern auch die passende @types-Version zu installieren, denn TypeScript-Kompatibilitätsprobleme entstehen häufig ausschließlich auf Typ-Ebene, während der Code zur Laufzeit einwandfrei läuft. Ein Build, der nur die Laufzeitbibliothek variiert, aber immer dieselben Typen verwendet, übersieht genau diese Klasse von Fehlern.
# .github/workflows/peer-matrix.yml
name: Peer Dependency Matrix
on: [push, pull_request]
jobs:
test:
strategy:
matrix:
react-version: ["17.0.2", "18.3.1", "19.0.0"]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npm install
- run: npm install react@${{ matrix.react-version }} react-dom@${{ matrix.react-version }} --no-save
- run: npm run typecheck
- run: npm test
7. Type-Only Imports für Peer Packages
Wenn eine TypeScript-Bibliothek ausschließlich Typen einer Peer Dependency benötigt, aber zur Laufzeit keinen direkten Zugriff auf deren JavaScript-Code, ist import type die richtige Wahl. Diese Syntax garantiert, dass der Import beim Kompilieren vollständig entfernt wird und keine Laufzeit-Abhängigkeit entsteht, selbst wenn ein Bundler versehentlich versucht, den regulären Import aufzulösen. Das ist besonders wichtig für optionale Peer Dependencies, bei denen der Konsument das Paket möglicherweise gar nicht installiert hat.
Ohne import type kann ein regulärer import-Ausdruck, der ausschließlich für Typinformationen gedacht war, in bestimmten Bundler-Konfigurationen dennoch zu einem echten Laufzeit-Import kompiliert werden, wenn isolatedModules aktiv ist und der Compiler nicht mit Sicherheit erkennen kann, dass der Import type-only ist. Explizites import type beseitigt diese Unsicherheit vollständig und macht die Absicht im Code auch für andere Entwickler sofort erkennbar.
// adapters/react-adapter.ts — type-only import, no runtime dependency
import type { ReactNode, ComponentType } from "react";
export interface FormFieldAdapter<TProps> {
component: ComponentType<TProps>;
render(props: TProps): ReactNode;
}
// Runtime code never touches "react" directly here —
// consumers who never install react still get correct typings.
8. Häufige Fehler: doppelte Typdefinitionen und Duplicate Identifier
Der mit Abstand häufigste Fehler bei Peer Dependencies in TypeScript ist die Fehlermeldung "Duplicate identifier" oder "Type X is not assignable to type X", obwohl beide Typen identisch benannt sind. Ursache ist praktisch immer, dass zwei unterschiedliche Kopien derselben @types-Pakete im node_modules-Baum existieren, entweder weil die Bibliothek die Typen fälschlich bündelt, oder weil unterschiedliche Versionsbereiche in einem Monorepo zu doppelter Installation führen.
Die Diagnose gelingt zuverlässig mit npm ls @types/react, das alle installierten Instanzen eines Typpakets im Abhängigkeitsbaum auflistet. Erscheint das Paket mehrfach mit unterschiedlichen Versionen, ist das die Fehlerursache. Die Lösung liegt fast immer darin, die eigene Peer-Dependency-Deklaration zu korrigieren oder in npm/pnpm/yarn gezielt eine einzige Version über Overrides beziehungsweise Resolutions zu erzwingen.
9. Peer-Dependency-Strategien im Vergleich
Je nach Art der Kopplung zwischen einer TypeScript-Bibliothek und ihrer Wirtsbibliothek eignen sich unterschiedliche Strategien. Die folgende Tabelle ordnet die gängigsten Situationen den passenden Ansätzen zu.
| Situation | Falscher Ansatz | Richtiger Ansatz |
|---|---|---|
| React-Plugin | react als dependency | react als peerDependency |
| Optionaler Vue-Adapter | Pflicht-Peer ohne Meta | peerDependenciesMeta.optional |
| Nur Typen benötigt | Normaler import | import type |
| Mehrere Major-Versionen | Nur gegen eine Version testen | CI-Matrix über alle Major-Versionen |
Diese vier Situationen decken den Großteil der Peer-Dependency-Fälle in typischen TypeScript-Bibliotheken ab. Wer sie konsequent unterscheidet, vermeidet die überwiegende Mehrheit der Support-Anfragen rund um Typkonflikte bei Konsumenten.
Mironsoft
TypeScript-Bibliotheken, Plugin-Architektur und CI-Testmatrizen
Peer-Dependency-Chaos in eurer Bibliothek beseitigen?
Wir analysieren bestehende Abhängigkeitsdeklarationen, richten korrekte Peer Dependencies mitsamt Testmatrix ein und beseitigen Duplicate-Identifier-Probleme bei euren Konsumenten dauerhaft.
Dependency-Audit
package.json auf falsch klassifizierte Peer Dependencies prüfen
CI-Matrix
Automatisierte Tests gegen mehrere Peer-Major-Versionen aufsetzen
Support-Reduktion
Duplicate-Identifier-Fehler bei Konsumenten systematisch vermeiden
10. Zusammenfassung
Peer Dependencies typsicher zu deklarieren bedeutet, drei Dinge gleichzeitig richtig zu machen: die richtige Abhängigkeitskategorie in package.json wählen, die zugehörigen @types-Pakete niemals ins veröffentlichte Paket bündeln, und Type-Only Imports konsequent für Peer Packages nutzen, deren Laufzeitcode optional bleibt. peerDependenciesMeta mit optional: true macht zusätzliche Frameworks zu echten Optionen statt zu Pflichtabhängigkeiten.
Der letzte Baustein ist eine CI-Testmatrix, die Behauptungen über unterstützte Versionsbereiche tatsächlich überprüft, statt sie nur in der package.json zu behaupten. Die häufigste Fehlerklasse, Duplicate Identifier durch doppelt installierte Typdefinitionen, lässt sich mit npm ls zuverlässig diagnostizieren und durch korrekte Peer-Dependency-Deklaration dauerhaft vermeiden.
Peer Dependencies typsicher deklarieren — Das Wichtigste auf einen Blick
Deklaration
Wirtsbibliotheken als peerDependencies, deren Typen ausschließlich als devDependencies eintragen.
Optionale Peers
peerDependenciesMeta.optional setzen und Laufzeitcode mit Guards gegen fehlende Pakete absichern.
Type-Only Imports
import type für Peer-Typen ohne Laufzeit-Abhängigkeit, garantiert vollständige Entfernung beim Build.
CI-Absicherung
Testmatrix über alle unterstützten Major-Versionen inklusive passender @types-Pakete.