Ein Architekturprinzip für robuste TypeScript-Anwendungen
Eine reine Validierungsfunktion, die nur true oder false zurückgibt, verwirft die einzige Information, die sie durch die Prüfung gewonnen hat, und zwingt den restlichen Code, dieselbe Prüfung an jeder weiteren Stelle zu wiederholen. Dieser Artikel erklärt das Prinzip Parsing statt Validieren, bekannt als Parse, Don't Validate, und zeigt anhand von Refinement Types, verschachtelten Parsern und praktischen TypeScript-Beispielen, wie sich unknown-Zustände dauerhaft aus einer Codebasis entfernen lassen, statt sie immer wieder neu zu prüfen.
Inhaltsverzeichnis
- 1. Parsing statt Validieren: Das Grundprinzip verstehen
- 2. Warum reine Validierung gewonnenes Wissen verwirft
- 3. Refinement Types: Information im Typ selbst verankern
- 4. Ein Parser als Funktion von unknown zu einem verfeinerten Typ
- 5. An der Grenze einmal parsen statt im Code wiederholt prüfen
- 6. Verschachtelte Parser für zusammengesetzte Domänenobjekte
- 7. Illegale Zustände unrepräsentierbar machen
- 8. Grenzen des Prinzips in der praktischen Anwendung
- 9. Validieren vs. Parsen im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Parsing statt Validieren: Das Grundprinzip verstehen
Das Prinzip Parsing statt Validieren, im englischen Original als Parse, Don't Validate bekannt, unterscheidet zwei fundamental verschiedene Reaktionen auf unsichere Eingabedaten. Eine Validierung prüft, ob ein Wert einer Bedingung entspricht, und gibt ein einfaches boolean zurück, das gewonnene Wissen über die tatsächliche Struktur des Werts geht dabei sofort wieder verloren. Ein Parser hingegen wandelt einen unsicheren Eingabewert in einen neuen, präziseren Typ um, der die Erkenntnis aus der Prüfung dauerhaft im Typsystem festhält.
Der Unterschied wirkt zunächst wie eine reine Formalität, hat aber weitreichende Konsequenzen für eine Codebasis. Wer konsequent parsed statt nur validiert, muss eine bestimmte Prüfung nur ein einziges Mal durchführen, an der Stelle, an der unsichere Daten in die Anwendung eintreten. Jede nachfolgende Funktion arbeitet mit einem bereits verfeinerten Typ und muss dieselbe Prüfung nie wieder wiederholen, ein struktureller Vorteil, den reine Validierung grundsätzlich nicht bieten kann.
2. Warum reine Validierung gewonnenes Wissen verwirft
Eine typische Validierungsfunktion sieht aus wie function isValidEmail(value: string): boolean. Nach einem erfolgreichen Aufruf weiß der Aufrufer zwar, dass die Bedingung zum Zeitpunkt der Prüfung erfüllt war, der Typ von value bleibt aber unverändert string. Jede weitere Funktion, die denselben Wert entgegennimmt, kann sich nicht auf diese Prüfung verlassen und muss entweder erneut validieren oder blind vertrauen, dass der Aufrufer bereits geprüft hat, eine Annahme, die in wachsenden Codebasen selten zuverlässig eingehalten wird.
Dieses Muster erzeugt in der Praxis eine wachsende Zahl an redundanten Prüfungen, die überall dort auftauchen, wo ein Wert erneut verwendet wird, weil niemand sich sicher genug ist, dass die ursprüngliche Validierung noch gilt. Reine Validierung beantwortet nur die Frage, ob ein Wert zu einem bestimmten Zeitpunkt gültig war, nicht die Frage, ob er es an jeder späteren Stelle im Code noch ist, ein Unterschied, der bei mutierbaren Datenstrukturen oder mehrstufigen Verarbeitungspipelines besonders ins Gewicht fällt.
// Validation: knowledge from the check is discarded immediately
function isValidEmail(value: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value);
}
function sendWelcomeEmail(rawEmail: string): void {
if (!isValidEmail(rawEmail)) {
throw new Error("Invalid email");
}
// rawEmail is still just "string" here, the check result is gone
dispatchEmail(rawEmail);
}
function dispatchEmail(email: string): void {
// This function has no compile-time guarantee that "email" was checked
// A caller could pass any string here without ever calling isValidEmail
}
3. Refinement Types: Information im Typ selbst verankern
Ein Refinement Type löst dieses Problem, indem er die Erkenntnis aus einer erfolgreichen Prüfung direkt im Typsystem sichtbar macht, statt sie in einem flüchtigen boolean zu verlieren. Statt einer Funktion, die true oder false zurückgibt, definiert ein Parser einen neuen, engeren Typ, etwa ValidatedEmail, der nur über einen kontrollierten Parsing-Schritt erzeugt werden kann.
Der entscheidende Unterschied gegenüber Branded Types allein ist, dass ein Refinement Type nicht nur nominal unterscheidbar ist, sondern seine Existenz selbst die Garantie liefert, dass die zugrunde liegende Bedingung erfüllt wurde. Eine Funktion, die einen ValidatedEmail als Parameter erwartet, kann sich vollständig darauf verlassen, dass dieser Wert bereits geprüft wurde, ohne die Prüfung selbst zu kennen oder zu wiederholen.
import { z } from "zod";
// The schema IS the parser: valid emails get their own distinguishable type
const validatedEmailSchema = z.string().email().brand<"ValidatedEmail">();
type ValidatedEmail = z.infer<typeof validatedEmailSchema>;
// Parsing: converts an unsafe string into a proven ValidatedEmail
function parseEmail(rawEmail: string): ValidatedEmail {
return validatedEmailSchema.parse(rawEmail);
}
function dispatchEmail(email: ValidatedEmail): void {
// The parameter type itself is proof that this value was already checked
console.log(`Sending to ${email}`);
}
function sendWelcomeEmail(rawEmail: string): void {
const email = parseEmail(rawEmail); // throws if invalid, narrows if not
dispatchEmail(email); // no re-validation possible, no re-validation needed
}
4. Ein Parser als Funktion von unknown zu einem verfeinerten Typ
Konzeptionell lässt sich ein Parser als Funktion mit der Signatur (input: unknown) => T beschreiben, die entweder einen Wert vom präzisen Typ T zurückgibt oder eine Exception wirft, statt in einem boolean-Zwischenschritt stehen zu bleiben. Diese Definition macht deutlich, warum Parsing statt Validieren kein bibliotheksspezifisches Konzept ist, sondern ein allgemeines Architekturprinzip, das mit Zod, Valibot oder sogar handgeschriebenen Funktionen gleichermaßen umgesetzt werden kann.
Zods .parse()-Methode ist ein direktes Beispiel für diese Signatur, ebenso wie jede handgeschriebene Funktion, die unknown entgegennimmt und entweder einen spezifischen Typ oder eine Exception liefert. Wichtig ist, dass ein Parser niemals einen unveränderten Eingabetyp zurückgibt, sonst wäre er lediglich eine Validierung mit zusätzlichem Exception-Mechanismus, aber kein echter Verfeinerungsschritt im Sinne des Prinzips.
5. An der Grenze einmal parsen statt im Code wiederholt prüfen
Die praktische Konsequenz von Parsing statt Validieren ist, den Parsing-Schritt so früh wie möglich durchzuführen, direkt an der Grenze, an der unsichere Daten in die Anwendung eintreten, sei es ein Request-Body, eine Konfigurationsdatei oder eine externe API-Antwort. Ab diesem Punkt trägt der geparste Wert einen präzisen Typ, und jede nachfolgende Funktion im Aufrufgraphen profitiert von dieser Garantie, ohne sie selbst herstellen zu müssen.
Dieses Muster steht im direkten Gegensatz zu einer Architektur, in der jede Funktion selbst prüft, ob ihre Eingabe gültig ist, ein Stil, der zu redundanten, oft leicht inkonsistenten Prüfungen über die gesamte Codebasis verteilt führt. Ein einmal geparster Wert muss seine Gültigkeit nie erneut beweisen, solange er innerhalb desselben, unveränderlichen Datenflusses bleibt.
import { z } from "zod";
const rawConfigSchema = z.object({
apiUrl: z.string().url(),
maxRetries: z.number().int().min(0).max(10),
timeoutMs: z.number().positive(),
});
type AppConfig = z.infer<typeof rawConfigSchema>;
// Parse once, at the boundary where the environment provides raw data
function loadConfig(rawEnv: unknown): AppConfig {
return rawConfigSchema.parse(rawEnv);
}
// Every function below trusts AppConfig completely, no re-checking needed
function createHttpClient(config: AppConfig) {
return {
baseUrl: config.apiUrl,
retries: config.maxRetries,
timeout: config.timeoutMs,
};
}
const config = loadConfig(JSON.parse(process.env.APP_CONFIG ?? "{}"));
const client = createHttpClient(config);
6. Verschachtelte Parser für zusammengesetzte Domänenobjekte
In komplexeren Domänenmodellen setzen sich Parser häufig aus kleineren Parsern zusammen, ein Muster, das direkt der Zusammensetzung von Zod-Schemas entspricht. Ein orderSchema kombiniert einen addressSchema und einen lineItemSchema, jedes Teilschema parsed unabhängig seinen eigenen Ausschnitt der Eingabedaten und wirft bei einem Fehler mit einem präzisen Pfad, der genau angibt, welches verschachtelte Feld ungültig war.
Dieser kompositionelle Charakter ist einer der praktischen Vorteile des Prinzips gegenüber einer einzigen, monolithischen Validierungsfunktion, die versucht, ein komplexes Objekt in einem Schritt zu prüfen. Kleinere, unabhängig testbare Parser lassen sich gezielt wiederverwenden, etwa denselben addressSchema sowohl für Liefer- als auch für Rechnungsadressen, ohne die Prüf-Logik zu duplizieren.
import { z } from "zod";
// Small, composable parsers, each responsible for one part of the domain
const addressSchema = z.object({
street: z.string().min(3),
postalCode: z.string().regex(/^\d{5}$/),
city: z.string().min(2),
});
const lineItemSchema = z.object({
sku: z.string(),
quantity: z.number().int().positive(),
unitPrice: z.number().positive(),
});
// Composed parser: reuses the smaller parsers without duplicating logic
const orderSchema = z.object({
shippingAddress: addressSchema,
billingAddress: addressSchema,
items: z.array(lineItemSchema).min(1),
});
type Order = z.infer<typeof orderSchema>;
function parseOrder(input: unknown): Order {
// A failing nested field produces a precise path, e.g. "shippingAddress.postalCode"
return orderSchema.parse(input);
}
7. Illegale Zustände unrepräsentierbar machen
Ein weiterführendes Ziel von Parsing statt Validieren ist, den Zieltyp eines Parsers so zu gestalten, dass ungültige Kombinationen von Feldern gar nicht erst darstellbar sind, statt sie zur Laufzeit abzufangen. Ein Beispiel: Statt eines Objekts mit einem optionalen Feld errorMessage: string | null neben einem status: "success" | "error", das theoretisch die widersprüchliche Kombination status: "success" mit gesetzter errorMessage erlaubt, modelliert eine Discriminated Union diese Fälle exklusiv.
Ein per Discriminated Union geparster Wert kann diesen widersprüchlichen Zustand gar nicht mehr einnehmen, der Parser garantiert strukturell, dass nur sinnvolle Kombinationen entstehen. Diese Idee, illegale Zustände unrepräsentierbar zu machen, ist die konsequente Weiterentwicklung des Grundprinzips: Nicht nur einzelne Werte werden verfeinert, sondern die gesamte Struktur eines Domänenobjekts wird so geformt, dass fehlerhafte Kombinationen strukturell ausgeschlossen sind.
8. Grenzen des Prinzips in der praktischen Anwendung
Parsing statt Validieren konsequent überall anzuwenden kann zu einer Explosion kleiner, spezifischer Typen führen, wenn jede noch so triviale Bedingung in einen eigenen Refinement Type verwandelt wird. Für viele einfache Fälle, etwa eine Zahl, die einfach nur positiv sein muss, ohne dass diese Eigenschaft im weiteren Code semantisch relevant ist, reicht eine gewöhnliche Validierung oft aus, ohne dass ein eigener Branded Type nötig wäre.
Eine sinnvolle Faustregel ist, das Prinzip konsequent an echten Systemgrenzen anzuwenden, Request-Bodies, Konfigurationsdateien, externe API-Antworten, und für rein interne, kurzlebige Zwischenwerte pragmatisch bei einfacher Validierung zu bleiben. Der Wert des Prinzips entfaltet sich vor allem dort, wo ein Datenwert über mehrere Funktionsgrenzen hinweg weitergereicht wird, nicht bei einer einzigen, lokal begrenzten Prüfung.
9. Validieren vs. Parsen im direkten Vergleich
Die folgende Tabelle stellt beide Ansätze anhand der Kriterien gegenüber, die in der Praxis am häufigsten über die Wahl entscheiden.
| Kriterium | Validieren (boolean) | Parsen (verfeinerter Typ) |
|---|---|---|
| Rückgabewert | boolean, Wissen geht verloren | Neuer, präziser Typ als Beweis |
| Wiederholte Prüfung | An jeder weiteren Verwendungsstelle nötig | Einmalig an der Grenze, danach garantiert |
| Fehlerlokalisierung | Meist nur ja/nein, wenig Kontext | Präziser Pfad bei verschachtelten Strukturen |
| Illegale Zustände | Bleiben strukturell darstellbar | Können durch den Zieltyp ausgeschlossen werden |
| Passend für | Triviale, lokal begrenzte Bedingungen | Werte, die Funktionsgrenzen überschreiten |
Beide Ansätze haben ihren Platz, aber sobald ein Wert seine ursprüngliche Funktion verlässt, ist ein geparster, verfeinerter Typ die robustere Grundlage für den restlichen Code.
Mironsoft
Parse-Don't-Validate-Architektur, Refinement Types und Domänenmodellierung in TypeScript
Redundante Prüfungen aus eurer Codebasis entfernen?
Wir analysieren, wo eure Anwendung dieselben Daten mehrfach validiert, führen Parsing an den echten Systemgrenzen ein und modellieren Domänenobjekte so, dass illegale Zustände unrepräsentierbar werden.
Architektur-Review
Redundante Validierungen entlang des Datenflusses identifizieren
Refinement Types
Verfeinerte Typen für kritische Domänenobjekte einführen
Domänenmodellierung
Discriminated Unions gegen illegale Zustandskombinationen
10. Zusammenfassung
Parsing statt Validieren verändert die Grundfrage, die eine Codebasis an unsichere Daten stellt, von einem flüchtigen "Ist dieser Wert gerade gültig" zu einem dauerhaften "Dieser Wert ist bewiesen gültig, solange sein Typ existiert". Refinement Types machen diese Erkenntnis im Typsystem sichtbar, sodass nachfolgende Funktionen einer bereits geparsten Eingabe vollständig vertrauen können, ohne dieselbe Prüfung erneut durchzuführen.
Verschachtelte, kompositionelle Parser skalieren dieses Prinzip auf komplexe Domänenobjekte, während Discriminated Unions illegale Zustandskombinationen strukturell ausschließen können, statt sie zur Laufzeit abzufangen. Der größte praktische Nutzen entsteht dort, wo ein Wert echte Funktionsgrenzen überschreitet, für rein lokale, triviale Prüfungen bleibt eine einfache Validierung weiterhin die pragmatischere Wahl.
Parsing statt Validieren, das Wichtigste auf einen Blick
Grundunterschied
Validieren gibt boolean zurück, Parsen erzeugt einen neuen, präziseren Typ.
Einmal an der Grenze
Parsing so früh wie möglich, danach gilt die Garantie überall im Code.
Komposition
Kleine Parser lassen sich zu komplexen Domänenobjekten zusammensetzen.
Illegale Zustände ausschließen
Discriminated Unions verhindern widersprüchliche Feldkombinationen strukturell.