Assertion Functions in TypeScript: eigene Typprüfungen mit asserts
AI generated
type
TypeScript · Type Narrowing
Assertion Functions: eigene Typprüfungen mit asserts
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.

10 Min. Lesezeit TypeScript 3.7+ Type Narrowing Control Flow Analysis

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

11. FAQ: Assertion Functions

1Was passiert, wenn eine Assertion Function nicht wirft, aber die Bedingung trotzdem falsch war?
TypeScript vertraut der Signatur vollständig. Ist die Implementierung fehlerhaft und wirft trotz falscher Bedingung nicht, entsteht eine falsche Typannahme im restlichen Code, die zur Laufzeit zu Fehlern führen kann, ohne dass der Compiler davor warnt.
2Kann eine Assertion Function auch einen Rückgabewert haben?
Nein. Der Rückgabetyp muss vollständig aus asserts condition oder asserts value is Type bestehen. Ein zusätzlicher Wert kann nicht gleichzeitig zurückgegeben werden, die Funktion signalisiert ausschließlich durch normales Zurückkehren oder Werfen.
3Funktionieren Assertion Functions auch mit async/await?
Nein, eine async-Funktion gibt immer ein Promise zurück, das erst zur Laufzeit aufgelöst wird. Die Kontrollfluss-Analyse von TypeScript arbeitet synchron zum Quellcode, weshalb asserts nur bei synchronen Funktionen unterstützt wird.
4Wie unterscheidet sich asserts condition von einem einfachen if mit throw direkt im Code?
Funktional identisch, aber asserts condition lässt sich in einer eigenen Funktion kapseln und wiederverwenden. Der wesentliche Vorteil liegt in der Wiederverwendbarkeit über mehrere Stellen im Code hinweg, nicht in einem grundsätzlich anderen Laufzeitverhalten.
5Sind Assertion Functions performanter als klassische Type Guards?
Nein, beide führen eine vergleichbare Laufzeitprüfung aus. Der Unterschied liegt ausschließlich in der statischen Typanalyse durch den Compiler, nicht in der tatsächlichen Ausführungsgeschwindigkeit zur Laufzeit.
6Kann ich asserts in einer Arrow Function verwenden?
Ja, solange die Arrow Function einer benannten Variable zugewiesen und explizit typisiert wird. Anonyme Arrow Functions, die direkt als Parameter übergeben werden, erkennt TypeScript aktuell nicht zuverlässig als Assertion Function.
7Was ist der Unterschied zwischen assert und der Node.js assert-Bibliothek?
Die Node.js assert-Bibliothek ist zur Laufzeit ähnlich, besitzt aber standardmäßig keine TypeScript-Assertion-Signatur. Erst mit einer eigenen Typdeklaration, die asserts condition verwendet, kann der Compiler sie als Assertion Function erkennen.
8Muss eine Assertion Function immer eine Error-Instanz werfen?
Technisch reicht jedes throw-Statement, unabhängig vom geworfenen Wert. In der Praxis sollte aus Gründen der Fehlerbehandlung und Stacktraces aber immer eine echte Error-Instanz oder eine Subklasse davon geworfen werden.
9Verlangsamen viele Assertion Functions die Anwendung spürbar?
Bei sinnvoll platzierten Prüfungen, etwa an Systemgrenzen, ist der Overhead vernachlässigbar. Werden sie jedoch in heißen Schleifen mit sehr vielen Iterationen eingesetzt, kann die wiederholte Prüfung messbar werden und sollte dann gezielt profiliert werden.
10Kann ich eine bestehende Type-Guard-Funktion einfach zu einer Assertion Function umbauen?
Ja, meist reicht es, die Bedingung umzukehren und statt eines booleschen Rückgabewerts eine Exception zu werfen, während die Signatur von value is Type auf asserts value is Type geändert wird. Aufrufstellen, die den booleschen Rückgabewert in einer if-Bedingung erwarten, müssen dabei angepasst werden.