Type Guards und Narrowing in TypeScript: Typen zur Laufzeit sicher eingrenzen

Warum weiß TypeScript in einem if-Block plötzlich, welchen Typ ein Wert hat? Lerne, wie Narrowing funktioniert und wie du mit eigenen Type Guards volle Typsicherheit gewinnst.

Teilen

Du arbeitest mit TypeScript und hast bestimmt schon Union-Typen wie string | number gesehen. Das Praktische daran: Sobald du innerhalb eines if-Blocks prüfst, um welchen Typ es sich handelt, kennt der Compiler dort auf einmal den genauen Typ und schlägt dir passende Methoden vor. Dieses Verhalten heißt Narrowing (Eingrenzen), und die Prüfungen, die es auslösen, nennt man Type Guards. In diesem Beitrag schauen wir uns an, wie das funktioniert und wie du sogar deine eigenen Type Guards schreibst.

Was ist Narrowing überhaupt?

TypeScript verfolgt für jede Variable, welcher Typ an einer bestimmten Stelle im Code möglich ist. Startest du mit einem Union-Typ, ist der Typ zunächst breit. Durch Prüfungen wie typeof, Vergleiche oder if-Abfragen grenzt der Compiler den Typ Schritt für Schritt ein. Innerhalb eines Zweigs weiß er dann, welche Eigenschaften und Methoden sicher verfügbar sind.

function verarbeite(wert: string | number) {
  if (typeof wert === "string") {
    // Hier ist wert garantiert ein string
    return wert.toUpperCase();
  }
  // Hier bleibt nur noch number übrig
  return wert.toFixed(2);
}

console.log(verarbeite("hallo")); // "HALLO"
console.log(verarbeite(3.14159)); // "3.14"

Der Clou: Du musst nichts casten. Der Compiler leitet den Typ aus dem Kontrollfluss ab. Würdest du im string-Zweig wert.toFixed() aufrufen, gäbe es sofort einen Fehler.

typeof, instanceof und der in-Operator

Für das Narrowing hast du mehrere eingebaute Werkzeuge. typeof eignet sich für primitive Typen wie "string", "number", "boolean" oder "undefined". Für Klassen-Instanzen nutzt du instanceof, und um zu prüfen, ob ein Objekt eine bestimmte Eigenschaft besitzt, hilft der in-Operator.

class Hund {
  bellen() { return "Wuff!"; }
}
class Katze {
  miauen() { return "Miau!"; }
}

function gibLaut(tier: Hund | Katze) {
  if (tier instanceof Hund) {
    return tier.bellen();
  }
  return tier.miauen();
}

// Der in-Operator prüft auf eine Eigenschaft
type Vogel = { fliegen: () => void };
type Fisch = { schwimmen: () => void };

function bewege(tier: Vogel | Fisch) {
  if ("fliegen" in tier) {
    tier.fliegen();
  } else {
    tier.schwimmen();
  }
}

Merke dir: instanceof funktioniert nur mit echten Klassen, weil es zur Laufzeit die Prototypenkette prüft. Für reine Objekt-Typen ist der in-Operator die richtige Wahl.

Truthiness und der Umgang mit null

Ein sehr häufiger Fall ist das Ausschließen von null oder undefined. TypeScript grenzt den Typ auch ein, wenn du einfach auf Wahrheitswert (truthiness) prüfst. Das ist besonders nützlich bei optionalen Werten.

function laenge(text: string | null | undefined): number {
  // Nach dieser Prüfung ist text garantiert ein string
  if (text) {
    return text.length;
  }
  return 0;
}

console.log(laenge("Code")); // 4
console.log(laenge(null));   // 0

Vorsicht: Bei Zahlen ist der Truthiness-Check tückisch, denn 0 gilt als falsy. Willst du wirklich nur null und undefined ausschließen, prüfe lieber explizit mit if (wert != null) – der lockere Vergleich fängt beide Fälle ab, lässt die Zahl 0 aber durch.

Eigene Type Guards mit "is" schreiben

Manchmal reichen die eingebauten Prüfungen nicht aus, etwa wenn du eine komplexe Struktur validierst. Dann schreibst du eine Funktion, deren Rückgabetyp ein Type Predicate ist – erkennbar an der Syntax wert is Typ. Gibt die Funktion true zurück, grenzt TypeScript den Typ im aufrufenden Code entsprechend ein.

type Nutzer = { name: string; email: string };

// Der Rückgabetyp "wert is Nutzer" ist der entscheidende Teil
function istNutzer(wert: unknown): wert is Nutzer {
  return (
    typeof wert === "object" &&
    wert !== null &&
    "name" in wert &&
    "email" in wert
  );
}

function begruesse(daten: unknown) {
  if (istNutzer(daten)) {
    // Dank des Type Guards ist daten hier ein Nutzer
    console.log(`Hallo, ${daten.name}!`);
  } else {
    console.log("Unbekannte Daten");
  }
}

begruesse({ name: "Anna", email: "anna@example.com" }); // Hallo, Anna!
begruesse("nur ein String"); // Unbekannte Daten

Solche Guards sind Gold wert, wenn du Daten aus einer API oder aus JSON verarbeitest, deren Form du zur Compile-Zeit nicht kennst. Du startest oft mit dem Typ unknown und arbeitest dich mit Guards zu einem sicheren Typ vor.

Discriminated Unions: Narrowing mit System

Ein besonders elegantes Muster sind Discriminated Unions. Dabei bekommt jeder Typ im Union eine gemeinsame Eigenschaft mit einem festen Literal-Wert – das sogenannte Discriminant-Feld. Über dieses Feld kann TypeScript in einem switch perfekt eingrenzen.

type Kreis = { art: "kreis"; radius: number };
type Rechteck = { art: "rechteck"; breite: number; hoehe: number };
type Form = Kreis | Rechteck;

function flaeche(form: Form): number {
  switch (form.art) {
    case "kreis":
      // form ist hier ein Kreis
      return Math.PI * form.radius ** 2;
    case "rechteck":
      // form ist hier ein Rechteck
      return form.breite * form.hoehe;
  }
}

console.log(flaeche({ art: "kreis", radius: 2 }).toFixed(2)); // "12.57"

Der große Vorteil: Fügst du später eine neue Form hinzu und behandelst sie nicht, kann dich TypeScript mit einer Exhaustiveness-Prüfung warnen. Dazu weist du im default-Zweig den Wert einer Variablen vom Typ never zu – bleibt ein Fall übrig, meckert der Compiler.

Fazit

Narrowing ist eines der Features, das TypeScript im Alltag so angenehm macht: Du prüfst einen Wert ganz normal mit typeof, instanceof, dem in-Operator oder einem Truthiness-Check, und der Compiler kennt danach den genauen Typ. Reichen die eingebauten Mittel nicht, schreibst du mit der wert is Typ-Syntax deine eigenen Type Guards und machst so selbst unsichere unknown-Daten typsicher. Und mit Discriminated Unions bekommst du ein robustes Muster für alle Fälle, in denen ein Wert mehrere klar unterscheidbare Formen annehmen kann. Probier es beim nächsten Mal aus, wenn du mit Union-Typen arbeitest – du wirst schnell merken, wie viel sicherer sich dein Code anfühlt.