Back

Type Narrowing in TypeScript verstehen

Type Narrowing in TypeScript verstehen

Sie haben einen Type Guard geschrieben, aber TypeScript beschwert sich immer noch, dass die Eigenschaft nicht existiert. Oder Sie haben ein Array gefiltert, doch der resultierende Typ bleibt eine Union. Diese Frustrationen entstehen aus einer Diskrepanz zwischen Ihrer Vorstellung davon, wie Narrowing funktioniert, und wie TypeScripts Control-Flow-Analyse tatsächlich arbeitet.

Dieser Artikel vermittelt ein klares mentales Modell von TypeScript Type Narrowing – wie der Compiler Typen durch Ihren Code hindurch verfolgt und wann diese Informationen verloren gehen.

Wichtigste Erkenntnisse

  • Type Narrowing ist TypeScripts Control-Flow-Analyse, die Typen entlang von Ausführungspfaden basierend auf verifizierbaren Laufzeitprüfungen verfolgt
  • Zentrale Narrowing-Mechanismen umfassen typeof, instanceof, Truthiness-Checks, Gleichheitsprüfungen, den in-Operator und Discriminated Unions
  • Benutzerdefinierte Type Guards mit Type Predicates ermöglichen individuelles Narrowing, aber TypeScript überprüft Ihre Prädikatlogik nicht
  • Der never-Typ ermöglicht Vollständigkeitsprüfungen zur Compile-Zeit, um unbehandelte Fälle aufzudecken
  • Narrowing versagt bei Callback-Grenzen, Property-Reassignments und komplexen Aliasing-Mustern

Wie Control-Flow-Analyse funktioniert

TypeScript Narrowing ist keine Magie. Der Compiler folgt den Ausführungspfaden Ihres Codes und aktualisiert Typinformationen basierend auf Laufzeitprüfungen, die er verifizieren kann.

Wenn Sie eine Bedingung schreiben, analysiert TypeScript, was in jedem Zweig wahr sein muss:

function process(value: string | number) {
  if (typeof value === 'string') {
    // TypeScript weiß: value ist hier string
    return value.toUpperCase()
  }
  // TypeScript weiß: value ist hier number
  return value.toFixed(2)
}

Der Compiler erkennt die typeof-Prüfung, identifiziert sie als Narrowing-Konstrukt und verfeinert den Typ entsprechend. Dies geschieht automatisch für mehrere JavaScript-Operatoren.

Zentrale Narrowing-Mechanismen

typeof und instanceof

Der typeof-Operator grenzt primitive Typen zuverlässig ein. TypeScript versteht seine Eigenheiten – einschließlich der Tatsache, dass typeof null den Wert "object" zurückgibt.

function handle(x: unknown) {
  if (typeof x === 'string') return x.length
  if (typeof x === 'number') return x.toFixed(2)
}

Für Klasseninstanzen bietet instanceof ein ähnliches Narrowing:

if (error instanceof TypeError) {
  console.log(error.message)
}

Truthiness und Gleichheit

Truthiness-Checks grenzen null und undefined aus:

function greet(name: string | null) {
  if (name) {
    return `Hello, ${name}`  // name ist string
  }
}

Gleichheitsprüfungen grenzen auf Literal-Typen ein:

type Status = 'pending' | 'complete' | 'failed'

function handle(status: Status) {
  if (status === 'complete') {
    // status ist 'complete'
  }
}

Der in-Operator und Discriminated Unions

Der in-Operator prüft auf Eigenschaftsexistenz und ermöglicht Duck-Typing-Muster:

if ('radius' in shape) {
  return Math.PI * shape.radius ** 2
}

Discriminated Unions kombinieren dies mit Literal-Typen für leistungsstarke Narrowing-Muster:

type Result<T> = 
  | { success: true; data: T }
  | { success: false; error: string }

function handle<T>(result: Result<T>) {
  if (result.success) {
    return result.data  // TypeScript weiß, dass data existiert
  }
  return result.error   // TypeScript weiß, dass error existiert
}

Benutzerdefinierte Type Guards

Wenn eingebaute Prüfungen nicht ausreichen, ermöglichen Type Predicates die Definition eigener Guards:

function isString(value: unknown): value is string {
  return typeof value === 'string'
}

Ein Vorbehalt: TypeScript überprüft Ihre Prädikatlogik nicht. Sie behaupten die Beziehung – wenn Ihre Prüfung falsch ist, werden die Typen falsch sein.

Modernes TypeScript (5.5+) hat sich hier verbessert. Array.filter kann nun Type Predicates für einfache Callbacks automatisch ableiten, was einen häufigen Schmerzpunkt eliminiert:

const mixed: (string | number)[] = ['a', 1, 'b', 2]
const strings = mixed.filter(x => typeof x === 'string')
// TypeScript leitet in vielen Fällen string[] ab

Vollständigkeitsprüfung mit never

Der never-Typ ermöglicht Vollständigkeitsprüfungen zur Compile-Zeit:

type Circle = { kind: 'circle'; radius: number }
type Square = { kind: 'square'; side: number }
type Triangle = { kind: 'triangle'; base: number; height: number }
type Shape = Circle | Square | Triangle

function area(shape: Shape): number {
  switch (shape.kind) {
    case 'circle': return Math.PI * shape.radius ** 2
    case 'square': return shape.side ** 2
    case 'triangle': return 0.5 * shape.base * shape.height
    default:
      const _exhaustive: never = shape
      return _exhaustive
  }
}

Wenn Sie eine neue Shape-Variante hinzufügen, meldet TypeScript einen Fehler bei der never-Zuweisung – und zwingt Sie, diese zu behandeln.

Was Narrowing nicht ist

Zwei Konstrukte, die oft mit Narrowing verwechselt werden, verdienen eine Klarstellung:

Type Assertions (as) umgehen das Typsystem vollständig. Sie grenzen nicht ein – sie überschreiben.

Der satisfies-Operator validiert, dass ein Ausdruck einem Typ entspricht, ohne ihn zu ändern. Nützlich zum Aufdecken von Fehlern, aber kein Narrowing-Mechanismus.

Wenn Narrowing versagt

TypeScripts Control-Flow-Analyse hat Grenzen. Narrowing bleibt nicht erhalten über:

  • Callback-Grenzen hinweg (obwohl Closures mit const-Variablen seit TypeScript 5.4+ Narrowing bewahren)
  • Property-Reassignments zwischen Prüfungen
  • Komplexe Aliasing-Muster

Wenn Narrowing unerwartet versagt, prüfen Sie, ob der Compiler tatsächlich den Control Flow von Ihrer Prüfung bis zur Verwendung nachverfolgen kann.

Fazit

Betrachten Sie Narrowing als TypeScripts Beobachtung Ihrer Laufzeitprüfungen und entsprechende Aktualisierung seines Wissens. Der Compiler ist konservativ – er grenzt nur ein, wenn er die Verfeinerung als korrekt beweisen kann.

Schreiben Sie Prüfungen, denen TypeScript folgen kann. Bevorzugen Sie Discriminated Unions gegenüber Type Assertions. Nutzen Sie Vollständigkeitsprüfungen, um fehlende Fälle zur Compile-Zeit statt zur Laufzeit aufzudecken.

Das Ziel ist nicht, gegen das Typsystem zu kämpfen, sondern Code so zu strukturieren, dass Narrowing natürlich funktioniert.

Häufig gestellte Fragen

TypeScripts Control-Flow-Analyse bewahrt Narrowing nicht über Callback-Grenzen hinweg, weil der Callback möglicherweise später ausgeführt wird, wenn sich der Typ der Variable geändert haben könnte. Um dies zu umgehen, weisen Sie den eingegrenzten Wert vor dem Callback einer const-Variable zu oder verwenden Sie einen Type Guard innerhalb des Callbacks selbst.

Ein Type Guard ist eine Laufzeitprüfung, die TypeScript erkennt und verwendet, um Typen sicher einzugrenzen. Eine Type Assertion mit 'as' weist TypeScript an, einen Wert als spezifischen Typ zu behandeln, ohne jegliche Laufzeitverifikation. Type Guards sind sicherer, weil sie tatsächliche Prüfungen beinhalten, während Assertions Bugs verbergen können, wenn Ihre Annahme falsch ist.

Verwenden Sie Discriminated Unions, wenn Sie verwandte Typen haben, die eine gemeinsame Literal-Eigenschaft wie 'kind' oder 'type' teilen. Sie bieten automatisches Narrowing in switch-Anweisungen und ermöglichen Vollständigkeitsprüfungen. Type Guards sind besser zum Validieren externer Daten oder wenn Sie die Typen, mit denen Sie arbeiten, nicht modifizieren können.

Dies bedeutet normalerweise, dass TypeScript den Überblick über Ihr Narrowing verloren hat. Häufige Ursachen sind: Prüfung einer Eigenschaft und dann Zugriff über eine andere Referenz, Neuzuweisung der Variable zwischen Prüfung und Verwendung, oder die Prüfung erfolgt in einem anderen Scope. Verschieben Sie Ihre Typprüfung näher an die Stelle, wo Sie den Wert verwenden, oder speichern Sie den eingegrenzten Wert in einer neuen const-Variable.

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay