Wie asserts dem Compiler mitteilt, dass eine Prüfung bereits stattgefunden hat
Assertion Functions erlauben es, Laufzeitprüfungen so zu deklarieren, dass der TypeScript-Compiler sie in seine Kontrollfluss-Analyse einbezieht. Dieser Artikel zeigt die Syntax mit asserts, den Unterschied zu klassischen Type Guards mit is und wann sich eigene Assertion Functions in der Praxis wirklich lohnen.
Inhaltsverzeichnis
- 1. Was Assertion Functions sind
- 2. Grundsyntax und einfaches Beispiel
- 3. asserts value is Type: gezielte Typverfeinerung
- 4. Unterschied zu klassischen Type Guards
- 5. Praxisfall: nicht-null-Zusicherungen ohne Non-Null-Assertion-Operator
- 6. Generische Assertion Functions für wiederverwendbare Validierung
- 7. Fallstricke und Grenzen
- 8. Best Practices für den Einsatz im Team
- 9. Assertion Functions im Vergleich zu verwandten Mechanismen
- 10. Zusammenfassung
- 11. FAQ
1. Was Assertion Functions sind
Eine Assertion Function ist eine Funktion, die entweder normal zurückkehrt oder eine Exception wirft, und deren Rückgabetyp mit dem Schlüsselwort asserts annotiert ist. Kehrt die Funktion zurück, ohne zu werfen, geht der Compiler davon aus, dass die im asserts-Ausdruck beschriebene Bedingung ab dieser Stelle im Code wahr ist. Das ist der zentrale Unterschied zu einer normalen Funktion mit boolean-Rückgabewert: Eine Assertion Function verändert den bekannten Typ einer Variablen dauerhaft für den restlichen Gültigkeitsbereich, nicht nur innerhalb eines if-Blocks.
Seit TypeScript 3.7 gibt es zwei Formen: asserts value is Type für die Typverfeinerung einer bestehenden Variable und die einfachere Form asserts condition, die lediglich aussagt, dass eine Bedingung wahr sein muss, ohne dabei den Typ einer bestimmten Variable zu verändern. Beide Formen werden ausschließlich in der Rückgabetyp-Position einer Funktionsdeklaration verwendet und funktionieren nicht als generischer Ausdruckstyp an anderer Stelle.
2. Grundsyntax und einfaches Beispiel
Die einfachste Form einer Assertion Function prüft lediglich, ob eine Bedingung erfüllt ist, und wirft andernfalls einen Fehler. Sie eignet sich für Vorbedingungen, die an mehreren Stellen im Code wiederholt vorkommen und die man nicht jedes Mal mit einem eigenen if-Block plus throw ausschreiben möchte.
Wichtig ist, dass der Compiler die Funktion tatsächlich als Assertion Function erkennt, also die Signatur exakt mit asserts beginnen muss. Eine Funktion, die intern zwar eine Exception wirft, aber nur mit void als Rückgabetyp deklariert ist, bringt dem Kontrollfluss-Analysierer nichts.
function assert(condition: unknown, message: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}
function processOrder(order: { total: number | null }) {
assert(order.total !== null, "Bestellsumme fehlt");
// Ab hier weiss der Compiler: order.total ist number, nicht number | null
console.log(order.total.toFixed(2));
}
3. asserts value is Type: gezielte Typverfeinerung
Die zweite Form kombiniert asserts mit is und benennt explizit, auf welchen Typ eine bestimmte Variable eingeschränkt wird. Das ist besonders nützlich, wenn eine Prüfung mehr als eine simple Bedingung ist, etwa eine Validierung mit mehreren Feldern, die als Ganzes einen bestimmten Objekttyp bestätigt.
Im folgenden Beispiel wird ein unbekanntes Objekt aus einer API-Antwort validiert. Nach dem Aufruf von assertIsUser behandelt der Compiler die Variable innerhalb der gesamten restlichen Funktion als User, ohne dass an jeder Nutzungsstelle erneut geprüft werden muss.
interface User {
id: number;
email: string;
}
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
typeof (value as User).id === "number" &&
typeof (value as User).email === "string"
);
}
function assertIsUser(value: unknown): asserts value is User {
if (!isUser(value)) {
throw new TypeError("Antwort entspricht nicht dem User-Schema");
}
}
function handleApiResponse(payload: unknown) {
assertIsUser(payload);
console.log(payload.email.toLowerCase()); // payload ist hier User
}
4. Unterschied zu klassischen Type Guards
Ein klassischer Type Guard mit value is Type als Rückgabetyp liefert einen boolean und wird typischerweise in einer Bedingung ausgewertet. Die Typverfeinerung gilt dann nur innerhalb des zugehörigen if-Zweigs, weil der Compiler die zwei Kontrollflusspfade, wahr und falsch, getrennt betrachtet.
Eine Assertion Function verfeinert dagegen den restlichen, linear folgenden Code, ohne dass eine Verzweigung nötig ist. Das ist praktisch, wenn eine Vorbedingung früh in einer Funktion geprüft werden soll und der restliche Funktionskörper dadurch nicht in ein tief verschachteltes if eingerückt werden muss. In der Praxis ergänzen sich beide Ansätze: Type Guards für Verzweigungslogik, Assertion Functions für Vorbedingungen und Guard Clauses.
// Type Guard: Verfeinerung nur im if-Zweig
function isString(value: unknown): value is string {
return typeof value === "string";
}
function withGuard(value: unknown) {
if (isString(value)) {
console.log(value.toUpperCase()); // nur hier ist value: string
}
// hier ist value wieder unknown
}
// Assertion Function: Verfeinerung für den Rest der Funktion
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new TypeError("Erwartete einen string");
}
}
function withAssertion(value: unknown) {
assertIsString(value);
console.log(value.toUpperCase()); // ab hier durchgehend value: string
}
5. Praxisfall: nicht-null-Zusicherungen ohne Non-Null-Assertion-Operator
Ein häufiger Anwendungsfall ist der Ersatz des Non-Null-Assertion-Operators !. Der Operator unterdrückt lediglich die Fehlermeldung des Compilers, ohne zur Laufzeit tatsächlich zu prüfen, ob ein Wert wirklich nicht null oder undefined ist. Eine kleine Assertion Function wie assertDefined übernimmt dieselbe Aufgabe, prüft aber tatsächlich zur Laufzeit und wirft bei Bedarf einen sprechenden Fehler.
Dieser Ansatz ist besonders in DOM-lastigem Code oder beim Zugriff auf optionale Konfigurationswerte nützlich, wo ein stiller undefined-Zugriff zu schwer auffindbaren Folgefehlern führen kann.
function assertDefined<T>(
value: T | null | undefined,
name: string
): asserts value is T {
if (value === null || value === undefined) {
throw new Error(`${name} darf nicht null oder undefined sein`);
}
}
function readConfigValue(config: Record<string, string | undefined>) {
const apiKey = config.apiKey;
assertDefined(apiKey, "config.apiKey");
return apiKey.trim(); // apiKey ist hier string, nicht string | undefined
}
6. Generische Assertion Functions für wiederverwendbare Validierung
Assertion Functions lassen sich mit Generics kombinieren, um wiederverwendbare Validierungsbausteine zu bauen, die an unterschiedliche Typen angepasst werden können. Eine generische Assertion Function für Array-Elemente ist ein gutes Beispiel: Sie prüft, dass jedes Element einer Liste einem bestimmten Prädikat entspricht, und verfeinert den Elementtyp des gesamten Arrays.
Zu beachten ist, dass der generische Typparameter dabei explizit über den Rückgabetyp mit asserts value is T[] ausgedrückt werden muss. Ohne diese Annotation würde TypeScript den generischen Parameter nicht korrekt für die Kontrollfluss-Analyse verwenden.
function assertAllMatch<T>(
values: unknown[],
predicate: (v: unknown) => v is T,
label: string
): asserts values is T[] {
const invalidIndex = values.findIndex((v) => !predicate(v));
if (invalidIndex !== -1) {
throw new TypeError(`${label}: ungültiges Element an Index ${invalidIndex}`);
}
}
function isNumber(value: unknown): value is number {
return typeof value === "number";
}
function sumScores(raw: unknown[]) {
assertAllMatch(raw, isNumber, "scores");
return raw.reduce((sum, n) => sum + n, 0); // raw ist hier number[]
}
7. Fallstricke und Grenzen
Assertion Functions dürfen keine Pfeilfunktionen mit implizitem this-Binding sein, wenn sie als Methode einer Klasse verwendet werden sollen, und sie funktionieren nicht als Rückgabewert eines höherwertigen Funktionsausdrucks: TypeScript erkennt asserts nur bei einer direkten Funktionsdeklaration oder einem klar benannten Funktionsausdruck, nicht bei anonymen Callbacks, die als Parameter durchgereicht werden.
Ein zweiter Fallstrick betrifft strikte Modus-Einstellungen: Ohne strictNullChecks verlieren viele Assertion Functions, die sich gegen null oder undefined richten, einen Großteil ihres Nutzens, weil der Compiler null ohnehin überall zulässt. Ein dritter, oft übersehener Punkt: Der Compiler vertraut der Assertion Function vollständig. Enthält die Implementierung selbst einen Logikfehler, etwa eine vertauschte Bedingung, entstehen falsche Typannahmen, die zur Laufzeit zu echten Fehlern führen, obwohl der Compiler keine Warnung ausgibt.
class Validator {
// Funktioniert: normale Methode
assertPositive(value: number): asserts value {
if (value <= 0) throw new Error("Wert muss positiv sein");
}
}
// Funktioniert NICHT zuverlässig: Assertion Function als Callback-Parameter
function runCheck(fn: (v: unknown) => asserts v is string) {
// TypeScript behandelt fn hier nicht als Assertion Signature
}
8. Best Practices für den Einsatz im Team
Assertion Functions sollten sprechende Namen mit dem Präfix assert tragen und im Fehlerfall eine präzise Fehlermeldung werfen, die den Kontext benennt, etwa den Feldnamen oder die aufrufende Funktion. Das erleichtert das Debugging erheblich gegenüber einer generischen Meldung wie invalid value.
Sinnvoll ist außerdem, Assertion Functions an Modulgrenzen zu konzentrieren, etwa direkt beim Einlesen externer Daten aus einer API oder Konfigurationsdatei. Innerhalb der Anwendung, wo Typen bereits durch die Typprüfung garantiert sind, sind sie meist überflüssig und würden nur unnötige Laufzeitkosten verursachen. Für Unit-Tests lohnt es sich, das Werfen der jeweiligen Exception explizit zu testen, damit Refactorings die Vorbedingung nicht versehentlich abschwächen.
9. Assertion Functions im Vergleich zu verwandten Mechanismen
Neben Assertion Functions bietet TypeScript weitere Werkzeuge zur Typverfeinerung, die je nach Situation besser oder schlechter passen. Die folgende Tabelle stellt die wichtigsten Mechanismen gegenüber und zeigt, wann welcher Ansatz die bessere Wahl ist.
Grundsätzlich gilt: Je kritischer die Laufzeitkonsequenzen eines falschen Typs sind, etwa bei Zahlungsdaten oder Authentifizierung, desto eher lohnt sich eine echte, prüfende Assertion Function gegenüber einer stillen Typbehauptung.
| Mechanismus | Laufzeitprüfung | Wirkungsbereich | Typischer Einsatz |
|---|---|---|---|
asserts condition |
Ja | Rest der Funktion | Vorbedingungen, Guard Clauses |
asserts value is T |
Ja | Rest der Funktion | Validierung externer Daten |
value is T (Type Guard) |
Ja | Nur im if-Zweig | Verzweigungslogik |
value as T |
Nein | Ab der Stelle, ohne Prüfung | Bekannte, aber unbewiesene Typen |
value! |
Nein | Ab der Stelle, ohne Prüfung | Kurzfristige, riskante Annahme |
Mironsoft
TypeScript-Migration, Typsicherheit und Team-Onboarding
JavaScript-Codebasis ohne Typsicherheit, aber keine Zeit für eine Rundum-Migration?
Wir migrieren bestehende JavaScript-Projekte schrittweise zu TypeScript, richten strikte Compiler-Einstellungen sauber ein und bringen Teams mit Code-Reviews und Style-Guides auf denselben Typsicherheits-Stand.
Migrations-Fahrplan
Schrittweise JS-zu-TS-Migration ohne Big-Bang-Risiko planen und umsetzen.
Strict-Mode-Einführung
tsconfig.json, ESLint-Regeln und CI-Checks für dauerhafte Typsicherheit aufsetzen.
Team-Onboarding
Entwickler mit Workshops und Code-Reviews in TypeScript-Best-Practices einarbeiten.
10. Zusammenfassung
Assertion Functions
Verfügbar seit
TypeScript 3.7
Schlüsselwort
asserts
Verhalten bei Fehlschlag
Muss eine Exception werfen
Wirkungsbereich
Restlicher Codepfad, nicht nur if-Zweig