Architecture Decision Records für TypeScript-Typentscheidungen im Team
AI generated
<T>
type
TypeScript · ADR · Typentscheidungen
Architecture Decision Records für TypeScript-Typentscheidungen
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.

16 Min. LesezeitADR · Generics · Discriminated Unions · Branded TypesTypeScript 5.x · Markdown · Team-Dokumentation

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.

DokumentationsformStärkeSchwäche bei TypentscheidungenEmpfehlung
Code-KommentarDirekt am Code sichtbarKein Platz für Alternativen und KonsequenzenAls Verweis auf ADR nutzen, nicht als Ersatz
Wiki-SeiteAusführlich möglichGetrennt vom Code, veraltet schnellFür Typentscheidungen ungeeignet
Pull-Request-BeschreibungEntsteht automatisch beim ReviewSchwer auffindbar Monate späterGuter Entwurfsort, aber kein Endziel
Architecture Decision RecordStabil nummeriert, im Repository versioniertBraucht Disziplin bei der ErstellungBeste 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.

11. FAQ: Architecture Decision Records für TypeScript-Typentscheidungen

1Was ist ein Architecture Decision Record für Typentscheidungen?
Ein kurzes Dokument, das Kontext, Entscheidung, Alternativen und Konsequenzen einer Typentscheidung festhält, damit sie später nachvollziehbar bleibt.
2Wie unterscheidet sich ein Typ-ADR von einem klassischen ADR?
Ein Typ-ADR ist schlanker und behandelt kleinere, häufigere Entscheidungen im Typsystem statt großer, seltener Architektur-Entscheidungen.
3Wann sollte ein Typ-ADR geschrieben werden?
Direkt im selben Pull Request, der die Typentscheidung einführt, solange der Kontext noch frisch im Gedächtnis ist.
4Wo sollten Typ-ADRs abgelegt werden?
Im selben Repository wie der Code, meist in einem docs/adr-Verzeichnis mit fortlaufender Nummerierung.
5Wie verlinkt man ein ADR mit dem betroffenen Code?
Über einen Kommentar mit der stabilen ADR-Nummer, niemals über einen Dateipfad, der sich später ändern kann.
6Was passiert mit einem ADR, wenn sich die Entscheidung ändert?
Es wird als superseded markiert und auf das neue ADR verwiesen, statt gelöscht zu werden, damit die Historie erhalten bleibt.
7Braucht jede Typentscheidung ein ADR?
Nein, nur Entscheidungen mit echten Alternativen und spürbaren Konsequenzen, nicht triviale oder offensichtliche Typwahlen.
8Wie lang sollte ein Typ-ADR sein?
Wenige Absätze reichen, ein Typ-ADR sollte in wenigen Minuten lesbar und schreibbar sein, keine ausführliche Abhandlung.
9Wie integriert man ADRs in Code-Reviews?
Ein Reviewer fordert bei ungewöhnlichen Typentscheidungen direkt ein kurzes ADR als Teil desselben Pull Requests ein.
10Ersetzt ein ADR die allgemeine Projektdokumentation?
Nein, ADRs ergänzen andere Dokumentationsformen an einer spezifischen Stelle: der Begründung einzelner Entscheidungen, nicht der Gesamtübersicht des Systems.