warum Generics, Unions und Branded Types Kontext brauchen
Ein Jahr nach einer Typentscheidung weiß im Team meist niemand mehr, warum ein bestimmtes Generic-Constraint oder eine Discriminated Union genau so modelliert wurde. Ein kurzes Architecture Decision Record hält Grund, Alternativen und Konsequenzen fest, damit Typentscheidungen nachvollziehbar bleiben, statt bei jedem Refactoring neu erraten zu werden.
Inhaltsverzeichnis
- 1. Warum Typentscheidungen eigene ADRs brauchen
- 2. Was ein Typ-ADR von einem klassischen ADR unterscheidet
- 3. Eine schlanke Vorlage für Typ-ADRs
- 4. Beispiel: Ein ADR für eine Generic-Constraint-Entscheidung
- 5. Beispiel: Ein ADR für eine Discriminated-Union-Modellierung
- 6. Wo ADRs abgelegt werden und wie man sie verlinkt
- 7. ADRs in den Review-Prozess integrieren
- 8. Umgang mit veralteten oder revidierten ADRs
- 9. ADRs im Vergleich zu anderen Dokumentationsformen
- 10. Zusammenfassung
- 11. FAQ
1. Warum Typentscheidungen eigene ADRs brauchen
Architecture Decision Records sind in der Softwareentwicklung längst etabliert für Entscheidungen wie Datenbankwahl oder Service-Schnitt, werden aber selten auf Typentscheidungen im Typsystem selbst angewendet. Dabei sind gerade Entscheidungen wie ein bestimmtes Generic-Constraint, eine Discriminated Union statt optionaler Felder oder ein Branded Type für eine spezielle ID genauso begründungsbedürftig wie jede andere Architektur-Entscheidung.
Ohne ein ADR verschwindet die Begründung hinter einer Typentscheidung meist innerhalb weniger Monate. Ein neues Teammitglied sieht nur das Ergebnis im Code, nicht die Diskussion, die dazu geführt hat, und riskiert, eine bewusste Design-Entscheidung versehentlich rückgängig zu machen, weil der Kontext fehlt.
Ein Typ-ADR schließt genau diese Lücke, indem es die Entscheidung, die Alternativen und die Konsequenzen an einem festen, auffindbaren Ort dokumentiert, statt sie in einem verschütteten Pull-Request-Kommentar oder einem vergessenen Slack-Thread zu belassen.
2. Was ein Typ-ADR von einem klassischen ADR unterscheidet
Ein klassisches ADR behandelt meist große, selten wiederkehrende Entscheidungen wie die Wahl eines Frameworks. Ein Typ-ADR hingegen behandelt kleinere, aber häufigere Entscheidungen im Typsystem, etwa warum ein bestimmter Typ als readonly modelliert wurde oder warum ein Generic auf ein bestimmtes Interface eingeschränkt ist. Das verlangt eine deutlich schlankere Vorlage als ein klassisches ADR mit ausführlichem Kontextabschnitt.
Ein weiterer Unterschied liegt im Bezug zum Code: Ein Typ-ADR sollte direkt neben der betroffenen Typdefinition verlinkt sein, meist über einen Kommentar mit Verweis auf die ADR-Nummer, statt nur in einem separaten Architektur-Dokument zu existieren, das niemand während der eigentlichen Codearbeit konsultiert.
Die Häufigkeit von Typ-ADRs unterscheidet sich ebenfalls deutlich von klassischen ADRs. Während ein Team pro Quartal vielleicht zwei oder drei klassische Architektur-Entscheidungen trifft, entstehen Typentscheidungen mit echtem Diskussionsbedarf deutlich öfter, sodass ein schwerfälliger Prozess hier schnell zur Hürde wird, die niemand mehr befolgt.
3. Eine schlanke Vorlage für Typ-ADRs
Eine wirksame Vorlage für Typ-ADRs braucht nur wenige Felder: Kontext, Entscheidung, Alternativen und Konsequenzen. Diese Kürze ist bewusst, weil ein Typ-ADR in wenigen Minuten geschrieben werden soll, direkt im selben Pull Request, der die Typentscheidung einführt, statt als separater nachträglicher Dokumentationsschritt.
Wichtig ist ein fortlaufender Index, damit jedes ADR über eine stabile Nummer referenzierbar bleibt, auch wenn sich der Dateiname oder der Speicherort später ändert. Diese Nummer taucht dann als Kommentar direkt im betroffenen Code auf und macht die Verbindung zwischen Entscheidung und Implementierung dauerhaft nachvollziehbar.
# ADR-0014: Branded type for validated order IDs
## Status
Accepted, 2026-06-02
## Context
Plain strings were used for order IDs across the checkout module. Two
production incidents happened because a raw string from an unrelated
context (a customer ID) was accidentally passed where an order ID was
expected. The compiler could not catch this, because both were just
`string`.
## Decision
Introduce a branded type `OrderId` that wraps `string` with a unique
brand field, produced only through a validating factory function.
## Alternatives considered
- A plain type alias `type OrderId = string` — rejected, provides no
compile-time protection against mixing up different string-based IDs.
- A full class wrapper — rejected, adds runtime overhead we don't need
for a type-level-only distinction.
- Runtime validation only (zod), no branded type — rejected, catches
the problem at the API boundary but not at internal call sites.
## Consequences
- Every order ID must now go through `toOrderId()`, adding one explicit
conversion step at system boundaries (API responses, form input).
- The compiler now rejects passing a `CustomerId` where an `OrderId`
is expected, closing the exact gap that caused the two incidents.
## Related
- ADR-0001 (branded types general approach)
- ADR-0015 (supersedes this decision, see section 8)
4. Beispiel: Ein ADR für eine Generic-Constraint-Entscheidung
Generic-Constraints wirken im Code oft wie eine reine Stilfrage, sind aber häufig das Ergebnis konkreter Erfahrungen mit fehlerhaften Aufrufen. Ein Typ-ADR, das dokumentiert, warum ein Generic auf ein Interface mit einer bestimmten Eigenschaft eingeschränkt wurde, verhindert, dass eine spätere Person die Einschränkung als unnötige Komplexität entfernt.
Der direkte Verweis im Code auf die ADR-Nummer ist hier besonders wertvoll, weil Generic-Constraints beim Lesen oft isoliert betrachtet werden, ohne den ursprünglichen Anwendungsfall zu kennen, der die Einschränkung nötig gemacht hat.
// See ADR-0009 for why this generic is constrained to HasTimestamp.
// Without the constraint, sortByDate() compiled but silently produced
// wrong results whenever T lacked a createdAt field (returned NaN).
interface HasTimestamp {
createdAt: Date;
}
function sortByDate<T extends HasTimestamp>(items: readonly T[]): T[] {
return [...items].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
}
// BEFORE ADR-0009 — this compiled without a constraint, and shipped a bug:
function sortByDateUnsafe<T>(items: readonly T[]): T[] {
return [...items].sort((a: any, b: any) => a.createdAt.getTime() - b.createdAt.getTime());
}
// Callers without createdAt silently produced NaN comparisons,
// which is exactly the incident that led the team to write ADR-0009.
const invoices: HasTimestamp[] = [{ createdAt: new Date("2026-01-01") }];
sortByDate(invoices);
5. Beispiel: Ein ADR für eine Discriminated-Union-Modellierung
Die Entscheidung, einen Zustand als Discriminated Union statt als Objekt mit mehreren optionalen Feldern zu modellieren, hat weitreichende Folgen für jeden nachfolgenden Zugriff im Code. Ein ADR, das diese Entscheidung begründet, verhindert, dass ein späteres Refactoring versehentlich zur ursprünglichen, fehleranfälligeren Struktur mit optionalen Feldern zurückkehrt.
Besonders wertvoll ist ein solches ADR, wenn die Discriminated Union zunächst komplizierter wirkt als die Alternative. Ohne dokumentierten Kontext liegt die Versuchung nahe, die vermeintliche Vereinfachung vorzunehmen und dabei genau die Garantie zu verlieren, die die Union ursprünglich bieten sollte.
Ein gutes Typ-ADR für eine solche Entscheidung nennt konkret, welche ungültigen Zustandskombinationen die optionalen Felder erlaubt hätten, etwa gleichzeitig loading und error gesetzt. Diese konkrete Aufzählung überzeugt mehr als der allgemeine Verweis auf bessere Typsicherheit, weil sie das reale Problem greifbar macht, das die Union verhindert.
6. Wo ADRs abgelegt werden und wie man sie verlinkt
Typ-ADRs sollten im selben Repository liegen wie der Code, den sie betreffen, meist in einem docs/adr-Verzeichnis mit fortlaufend nummerierten Markdown-Dateien. Das stellt sicher, dass ADRs denselben Review-Prozess durchlaufen wie Code-Änderungen und bei einem Fork oder Checkout automatisch mit ausgeliefert werden.
Die Verlinkung im Code sollte immer die stabile ADR-Nummer nutzen, niemals einen Dateipfad, der sich bei einer Reorganisation des docs-Verzeichnisses ändern kann. Ein einfacher Kommentar wie See ADR-0014 reicht aus, solange die Nummerierung im Team konsistent gepflegt wird.
Eine kurze Indexdatei, die jeden ADR-Titel neben seiner Nummer auflistet und am Anfang des docs/adr-Verzeichnisses liegt, hilft neuen Teammitgliedern, vergangene Typentscheidungen zu überblicken, ohne jede einzelne Datei öffnen zu müssen. Dieser Index kostet pro ADR nur eine Minute Pflegeaufwand und zahlt sich beim ersten Mal aus, wenn jemand nach früherem Kontext sucht.
# docs/adr/ — one file per decision, numbered consecutively, never reused
docs/adr/
0001-use-branded-types-for-ids.md
0009-generic-constraint-on-sortbydate.md
0014-branded-type-for-order-ids.md
0015-supersedes-0014-order-id-as-uuid-wrapper.md
# A small script keeps the next-number lookup trivial for contributors
next_adr_number() {
ls docs/adr | grep -oE '^[0-9]+' | sort -n | tail -1 | awk '{printf "%04d\n", $1+1}'
}
# Usage when starting a new ADR:
new_number=$(next_adr_number)
touch "docs/adr/${new_number}-short-decision-title.md"
echo "Created docs/adr/${new_number}-short-decision-title.md"
7. ADRs in den Review-Prozess integrieren
Ein ADR, das erst Wochen nach der eigentlichen Typentscheidung geschrieben wird, verliert an Genauigkeit, weil sich Details der Diskussion bereits verflüchtigt haben. Der wirksamste Zeitpunkt ist der Pull Request selbst: Ein Reviewer, der eine ungewöhnliche Typentscheidung sieht, kann direkt ein kurzes ADR als Teil desselben Pull Requests einfordern, statt die Entscheidung ohne Kontext zu genehmigen.
Diese Integration in den Review-Prozess macht ADRs zu einem selbstverständlichen Teil der Arbeit, statt zu einer separaten Dokumentationsaufgabe, die nach Feature-Fertigstellung meist keine Priorität mehr hat. Ein Team, das diese Praxis etabliert, merkt schnell, dass die meisten ADRs in unter zehn Minuten geschrieben sind, wenn der Kontext noch frisch ist.
<!-- .github/PULL_REQUEST_TEMPLATE.md — excerpt -->
## Type decisions
- [ ] This PR introduces a non-obvious type pattern (generic constraint,
discriminated union, branded type, or similar)
- [ ] If checked above: an ADR is included in this PR under `docs/adr/`
- [ ] The ADR number is referenced in a comment next to the type definition
- [ ] The ADR lists at least one alternative that was considered and rejected
- [ ] Reviewer: if an unusual type pattern is not backed by an ADR, request
one before approving, not after merge
8. Umgang mit veralteten oder revidierten ADRs
Typentscheidungen ändern sich, wenn sich Anforderungen ändern, und ein altes ADR sollte dann nicht gelöscht, sondern als superseded markiert werden. Das erhält den historischen Kontext, warum die ursprüngliche Entscheidung getroffen wurde, und verweist gleichzeitig auf das neue ADR, das die aktuelle Entscheidung begründet.
Ein gelöschtes ADR reißt eine Lücke in die Historie und lässt spätere Teammitglieder wieder im Dunkeln, warum überhaupt eine Änderung nötig war. Ein als superseded markiertes ADR hingegen erzählt die vollständige Geschichte einer Typentscheidung über mehrere Iterationen hinweg.
# ADR-0014: Branded type for validated order IDs
> STATUS: Superseded by ADR-0015 (2026-09-12)
> Reason: order IDs became UUIDs after the checkout migration, the
> regex-based validation in toOrderId() no longer applied.
> See ADR-0015 for the current decision and its rationale.
## Context
(original context preserved below for historical reference,
do not delete even though the decision itself no longer applies)
...
9. ADRs im Vergleich zu anderen Dokumentationsformen
ADRs konkurrieren nicht mit anderen Dokumentationsformen, sondern ergänzen sie an einer spezifischen Stelle: der Begründung einzelner Entscheidungen. Die folgende Übersicht ordnet ADRs gegenüber Alternativen ein, die Teams häufig anstelle von oder zusätzlich zu ADRs nutzen.
Der entscheidende Maßstab beim Vergleich ist nicht, welche Form am ausführlichsten dokumentiert, sondern welche Form in einem Jahr noch auffindbar und vertrauenswürdig ist. Genau an diesem Maßstab schneiden ADRs deutlich besser ab als spontane Notizen in Chat-Tools oder Pull-Request-Kommentaren.
| Dokumentationsform | Stärke | Schwäche bei Typentscheidungen | Empfehlung |
|---|---|---|---|
| Code-Kommentar | Direkt am Code sichtbar | Kein Platz für Alternativen und Konsequenzen | Als Verweis auf ADR nutzen, nicht als Ersatz |
| Wiki-Seite | Ausführlich möglich | Getrennt vom Code, veraltet schnell | Für Typentscheidungen ungeeignet |
| Pull-Request-Beschreibung | Entsteht automatisch beim Review | Schwer auffindbar Monate später | Guter Entwurfsort, aber kein Endziel |
| Architecture Decision Record | Stabil nummeriert, im Repository versioniert | Braucht Disziplin bei der Erstellung | Beste Wahl für Typentscheidungen |
10. Zusammenfassung
Ein Architecture Decision Record für Typentscheidungen schließt eine Lücke, die klassische ADRs offen lassen: die Begründung kleiner, aber folgenreicher Entscheidungen im Typsystem selbst. Eine schlanke Vorlage mit Kontext, Entscheidung, Alternativen und Konsequenzen reicht aus, solange das ADR direkt im selben Pull Request entsteht, der die Typentscheidung einführt.
Die Verlinkung im Code über eine stabile ADR-Nummer und die Integration in den Review-Prozess machen aus einer einmaligen Dokumentationsaufgabe eine selbstverständliche Praxis. Teams, die veraltete ADRs als superseded markieren statt sie zu löschen, erhalten eine vollständige, nachvollziehbare Geschichte ihrer Typentscheidungen über Jahre hinweg.
ADRs für TypeScript-Typentscheidungen, das Wichtigste auf einen Blick
Schlanke Vorlage
Kontext, Entscheidung, Alternativen, Konsequenzen genügen, ein Typ-ADR muss in Minuten schreibbar sein.
Direkt im Pull Request
Der beste Zeitpunkt ist während der eigentlichen Typentscheidung, nicht Wochen danach.
Stabile Verlinkung im Code
Ein Kommentar mit ADR-Nummer verbindet Entscheidung und Implementierung dauerhaft.
Superseded statt gelöscht
Veraltete ADRs bleiben als Historie erhalten und verweisen auf die aktuelle Entscheidung.