Die is-Syntax richtig einsetzen
Eigene Type Predicates erlauben es, dem TypeScript-Compiler mit einer selbst geschriebenen Funktion mitzuteilen, welchen konkreten Typ ein Wert nach einer Prüfung tatsächlich hat. Richtig eingesetzt ersetzen sie manuelle Type Assertions durch echte, vom Compiler nachvollziehbare Narrowing-Logik.
Inhaltsverzeichnis
- 1. Was ein Type Predicate ist und wofür die is-Syntax steht
- 2. Eine einfache eigene Type-Guard-Funktion schreiben
- 3. Type Predicates für komplexe Union Types
- 4. Assertion Functions als Ergänzung: asserts x is T
- 5. Type Predicates in Array.filter nutzen
- 6. Generische Type Predicates schreiben
- 7. Fallstricke: unsichere Type Predicates als Typ-Lügen
- 8. Eigene Predicates vs. in-Operator, typeof und instanceof
- 9. Praxisbeispiel: API-Response-Validierung mit Type Predicates
- 10. Zusammenfassung
- 11. FAQ
1. Was ein Type Predicate ist und wofür die is-Syntax steht
Eingebaute Narrowing-Mechanismen wie typeof, instanceof oder Gleichheitsvergleiche funktionieren nur, solange die Prüfung direkt im selben Ausdruck steht, den der Compiler analysiert. Sobald die Prüfung in eine eigene Funktion ausgelagert wird, geht diese Information normalerweise verloren.
Ein Type Predicate löst dieses Problem, indem der Rückgabetyp der Funktion nicht boolean, sondern parameterName is Type lautet. Der Compiler behandelt einen true-Rückgabewert dann so, als hätte die Prüfung direkt an der Aufrufstelle stattgefunden.
Die Funktion selbst führt zur Laufzeit eine ganz normale boolesche Prüfung durch, meistens über typeof, in oder den Vergleich einzelner Properties. Die is-Syntax ist reine Typebene-Information für den Compiler, ohne jede Laufzeitwirkung.
function isString(value: unknown): value is string {
return typeof value === "string";
}
function printLength(value: unknown) {
if (isString(value)) {
console.log(value.length); // value ist hier als string bekannt
}
}
2. Eine einfache eigene Type-Guard-Funktion schreiben
Für benutzerdefinierte Klassen oder Interfaces prüft ein Type Predicate typischerweise, ob ein bestimmtes Property existiert und den erwarteten Typ hat, meist mit dem in-Operator kombiniert mit einer Typprüfung auf dessen Wert.
Wichtig ist, dass der Parametername im Predicate exakt dem Parameternamen der Funktion entspricht: value is Dog funktioniert nur für einen Parameter, der tatsächlich value heißt, nicht für einen anders benannten.
Type Predicates lassen sich auf jede Funktion anwenden, unabhängig davon, ob sie als benannte Funktion, als Arrow Function oder als Methode einer Klasse definiert ist, solange die Signatur den is-Rückgabetyp trägt.
interface Dog {
bark(): void;
}
interface Cat {
meow(): void;
}
function isDog(animal: Dog | Cat): animal is Dog {
return "bark" in animal;
}
function makeSound(animal: Dog | Cat) {
if (isDog(animal)) {
animal.bark();
} else {
animal.meow();
}
}
3. Type Predicates für komplexe Union Types
Bei Discriminated Unions mit einem gemeinsamen Feld wie kind oder type genügt oft ein direkter Vergleich im if, ohne eigene Funktion. Ein eigenes Predicate lohnt sich trotzdem, sobald dieselbe Prüfung an mehreren Stellen im Code wiederverwendet wird.
Für Unions ohne gemeinsames Discriminator-Feld ist ein eigenes Predicate häufig die einzige praktikable Lösung, weil der Compiler die Zugehörigkeit sonst nicht automatisch anhand der Struktur ableiten kann, besonders bei überlappenden optionalen Properties.
Ein zentrales, gut benanntes Predicate wie isSuccessResponse macht solchen Code außerdem lesbarer als ein wiederholter, mehrzeiliger Strukturvergleich direkt an jeder Aufrufstelle.
interface SuccessResponse {
status: "success";
data: unknown;
}
interface ErrorResponse {
status: "error";
message: string;
}
type ApiResponse = SuccessResponse | ErrorResponse;
function isSuccessResponse(res: ApiResponse): res is SuccessResponse {
return res.status === "success";
}
4. Assertion Functions als Ergänzung: asserts x is T
Neben Type Predicates, die einen booleschen Rückgabewert liefern, kennt TypeScript Assertion Functions mit der Syntax asserts value is Type, die statt eines Werts eine Exception werfen, falls die Bedingung nicht erfüllt ist.
Nach dem Aufruf einer solchen Assertion-Funktion behandelt der Compiler den restlichen Code im aktuellen Block so, als wäre die Bedingung garantiert erfüllt, ganz ohne umschließendes if, weil ein Fehlschlag bereits eine Exception ausgelöst hätte.
Assertion Functions eignen sich besonders für Vorbedingungen am Anfang einer Funktion, etwa zur Validierung von Konfigurationswerten oder API-Eingaben, wo ein if-return-Muster den Kontrollfluss unnötig verschachteln würde.
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error("Expected a string");
}
}
function process(value: unknown) {
assertIsString(value);
console.log(value.toUpperCase()); // value ist hier bereits als string bekannt
}
5. Type Predicates in Array.filter nutzen
Ohne Type Predicate liefert array.filter(item => item !== null) weiterhin ein Array mit dem ursprünglichen, nullable Elementtyp, weil TypeScript den Zusammenhang zwischen der Filterbedingung und dem Ergebnistyp nicht automatisch erkennt.
Wird die Filterfunktion stattdessen als eigenes Type Predicate geschrieben, erkennt TypeScript, dass jedes verbleibende Element garantiert dem engeren Typ entspricht, und passt den Ergebnistyp von filter() entsprechend an.
Dieses Muster ist einer der häufigsten praktischen Einsatzzwecke für Type Predicates überhaupt, weil es null- oder undefined-Filterung aus Arrays typsicher macht, ohne im Anschluss eine zusätzliche Type Assertion zu benötigen.
function isDefined<T>(value: T | undefined): value is T {
return value !== undefined;
}
const values: (string | undefined)[] = ["a", undefined, "b"];
const defined: string[] = values.filter(isDefined); // korrekt typisiert
6. Generische Type Predicates schreiben
Wie im vorherigen Beispiel mit isDefined gezeigt, lassen sich Type Predicates mit generischen Typparametern kombinieren, um wiederverwendbare Guards zu schreiben, die für beliebige konkrete Typen funktionieren, statt an einen festen Typ gebunden zu sein.
Ein häufiges Muster ist ein generisches Predicate, das prüft, ob ein Wert eine Instanz einer bestimmten Klasse ist, kombiniert mit einem zweiten generischen Parameter für den Klassen-Konstruktor selbst, um Wiederholung über mehrere konkrete Klassen hinweg zu vermeiden.
Generische Predicates sind besonders wertvoll in Utility-Bibliotheken, die von vielen Projekten importiert werden, weil eine einzige, gut getestete Implementierung Dutzende von projektspezifischen, sonst duplizierten Guards ersetzt und dadurch die Wartungslast über das gesamte Team hinweg spürbar senkt.
function isInstanceOf<T>(
value: unknown,
ctor: new (...args: never[]) => T
): value is T {
return value instanceof ctor;
}
class ValidationError extends Error {}
function handle(err: unknown) {
if (isInstanceOf(err, ValidationError)) {
console.log(err.message); // err ist hier ValidationError
}
}
7. Fallstricke: unsichere Type Predicates als Typ-Lügen
Ein Type Predicate ist eine reine Zusicherung an den Compiler, keine vom Compiler verifizierte Garantie. Wenn die tatsächliche Prüfung im Funktionskörper nicht wirklich zum behaupteten Typ passt, entsteht eine Typ-Lüge, die der Compiler nicht erkennen kann.
Ein klassisches Beispiel ist ein Predicate, das nur die Existenz eines Properties prüft, aber nicht dessen tatsächlichen Typ, etwa "id" in obj als vermeintlicher Beweis für obj is User, obwohl id auch bei völlig anderen Objektformen vorkommen kann.
Weil ein fehlerhaftes Predicate zur Laufzeit keinen Fehler wirft, sondern stillschweigend falsche Typinformationen an nachfolgenden Code weitergibt, lohnt sich für jedes nicht-triviale Predicate ein eigener Unit-Test, der sowohl positive als auch negative Fälle abdeckt, inklusive Grenzfällen wie leeren Objekten oder unerwarteten zusätzlichen Properties.
8. Eigene Predicates vs. in-Operator, typeof und instanceof
Für einfache Fälle, primitive Typen oder bekannte Klassen, reichen typeof und instanceof direkt im if völlig aus und benötigen kein eigenes Predicate, weil der Compiler diese Operatoren bereits nativ für Narrowing versteht.
Der in-Operator narrowt zwar ebenfalls automatisch, aber nur anhand der Existenz eines Properties, nicht anhand seines Typs, weshalb er bei überlappenden Interfaces mit gleichnamigen, aber unterschiedlich typisierten Properties an seine Grenzen stößt.
Ein eigenes Type Predicate lohnt sich vor allem dann, wenn die Prüflogik komplexer ist als ein einzelner nativer Operator abdecken kann, oder wenn dieselbe Prüfung an mehreren Stellen im Code wiederverwendet werden soll, statt sie jedes Mal neu auszuschreiben.
9. Praxisbeispiel: API-Response-Validierung mit Type Predicates
Bei der Verarbeitung von JSON-Antworten aus externen APIs kennt TypeScript naturgemäß keine statische Information über die tatsächliche Struktur der Daten, weshalb fetch().json() typischerweise any zurückgibt.
Ein Type Predicate, das die minimal erwartete Struktur zur Laufzeit tatsächlich prüft, etwa über mehrere typeof- und in-Checks, verbindet echte Laufzeitvalidierung mit einer präzisen, vom Compiler nutzbaren Typinformation im Anschluss.
Für komplexere Schemas lohnt sich in der Praxis oft eine Validierungsbibliothek wie Zod, deren .parse()- oder .safeParse()-Methoden intern dasselbe Prinzip nutzen wie ein selbst geschriebenes Type Predicate, nur generiert aus einer deklarativen Schema-Definition statt manuell programmiert, was Tippfehler und vergessene Feldprüfungen deutlich reduziert.
interface ApiUser {
id: number;
name: string;
}
function isApiUser(data: unknown): data is ApiUser {
return (
typeof data === "object" &&
data !== null &&
"id" in data &&
typeof (data as Record<string, unknown>).id === "number" &&
"name" in data &&
typeof (data as Record<string, unknown>).name === "string"
);
}
async function loadUser(url: string): Promise<ApiUser> {
const data: unknown = await (await fetch(url)).json();
if (!isApiUser(data)) {
throw new Error("Unerwartete API-Antwort");
}
return data;
}
| Methode | Narrowing automatisch? | Wiederverwendbar? | Typischer Einsatz |
|---|---|---|---|
| typeof | Ja, nativ | Nein, inline | Primitive Typen wie string, number |
| instanceof | Ja, nativ | Nein, inline | Klasseninstanzen |
| in-Operator | Ja, nativ, nur nach Existenz | Nein, inline | Property-Existenz in Union-Typen |
| Eigenes Type Predicate (is) | Ja, über Funktionsaufruf | Ja, zentral definierbar | Komplexe Prüfungen, Array.filter |
| Assertion Function (asserts) | Ja, ohne umschließendes if | Ja, zentral definierbar | Vorbedingungen, Eingabevalidierung |
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
Eigene Type Predicates
is-Syntax
parameterName is Type statt boolean
Nur Compile-Zeit
Keine automatische Laufzeitprüfung
filter()-Muster
Entfernt null/undefined typsicher
Testpflicht
Fehlerhafte Predicates sind Typ-Lügen