Type Guards
Lerne Type Guards in TypeScript für sichere Typ-Eingrenzung zur Laufzeit.
Aktualisiert:
Type Guards
Type Guards ermöglichen es dir, den Typ einer Variable zur Laufzeit einzugrenzen. TypeScript versteht diese Prüfungen und passt den Typ im entsprechenden Code-Block automatisch an.
typeof Type Guard
Der einfachste Type Guard für primitive Typen:
function process(value: string | number): string {
if (typeof value === "string") {
// TypeScript weiß: value ist string
return value.toUpperCase();
}
// TypeScript weiß: value ist number
return value.toFixed(2);
}
console.log(process("hello")); // "HELLO"
console.log(process(42.5)); // "42.50"
Mehrere typeof Checks
function describe(value: string | number | boolean): string {
if (typeof value === "string") {
return `String with length ${value.length}`;
}
if (typeof value === "number") {
return `Number: ${value}`;
}
// TypeScript weiß: value ist boolean
return `Boolean: ${value}`;
}
instanceof Type Guard
Für Klassen und Konstruktoren:
class Dog {
bark(): string {
return "Woof!";
}
}
class Cat {
meow(): string {
return "Meow!";
}
}
function makeSound(animal: Dog | Cat): string {
if (animal instanceof Dog) {
return animal.bark(); // TypeScript weiß: animal ist Dog
}
return animal.meow(); // TypeScript weiß: animal ist Cat
}
console.log(makeSound(new Dog())); // "Woof!"
console.log(makeSound(new Cat())); // "Meow!"
in Operator Type Guard
Prüft, ob ein Property existiert:
type Bird = {
fly: () => void;
layEggs: () => void;
};
type Fish = {
swim: () => void;
layEggs: () => void;
};
function move(animal: Bird | Fish): void {
if ("fly" in animal) {
// TypeScript weiß: animal ist Bird
animal.fly();
} else {
// TypeScript weiß: animal ist Fish
animal.swim();
}
}
Truthiness Narrowing
function printName(name: string | null | undefined): void {
if (name) {
// TypeScript weiß: name ist string (nicht null/undefined)
console.log(name.toUpperCase());
} else {
console.log("No name provided");
}
}
// Mit Array
function processItems(items: string[] | undefined): void {
if (items?.length) {
// items existiert und hat Elemente
items.forEach(item => console.log(item));
}
}
Discriminated Unions
Das mächtigste Pattern für Type Guards:
type LoadingState = {
status: "loading";
};
type SuccessState = {
status: "success";
data: string;
};
type ErrorState = {
status: "error";
error: Error;
};
type State = LoadingState | SuccessState | ErrorState;
function handleState(state: State): string {
switch (state.status) {
case "loading":
return "Loading...";
case "success":
// TypeScript weiß: state hat 'data'
return `Data: ${state.data}`;
case "error":
// TypeScript weiß: state hat 'error'
return `Error: ${state.error.message}`;
}
}
Custom Type Guards
Mit is Keyword definierst du eigene Type Guards:
type Cat = { meow: () => void; name: string };
type Dog = { bark: () => void; name: string };
// Custom Type Guard
function isCat(animal: Cat | Dog): animal is Cat {
return "meow" in animal;
}
function isDog(animal: Cat | Dog): animal is Dog {
return "bark" in animal;
}
function handleAnimal(animal: Cat | Dog): void {
if (isCat(animal)) {
animal.meow(); // TypeScript weiß: animal ist Cat
} else {
animal.bark(); // TypeScript weiß: animal ist Dog
}
}
Type Guard für Arrays
function isStringArray(value: unknown): value is string[] {
return (
Array.isArray(value) &&
value.every(item => typeof item === "string")
);
}
function processInput(input: unknown): void {
if (isStringArray(input)) {
// TypeScript weiß: input ist string[]
input.forEach(str => console.log(str.toUpperCase()));
}
}
Type Guard für Objekte
type User = {
id: number;
name: string;
email: string;
};
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value &&
"email" in value &&
typeof (value as User).id === "number" &&
typeof (value as User).name === "string" &&
typeof (value as User).email === "string"
);
}
function processData(data: unknown): void {
if (isUser(data)) {
console.log(`User: ${data.name} (${data.email})`);
}
}
Assertion Functions
Eine Alternative zu Type Guards (TypeScript 3.7+):
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error("Value must be a string");
}
}
function assertIsDefined<T>(value: T): asserts value is NonNullable<T> {
if (value === null || value === undefined) {
throw new Error("Value must be defined");
}
}
function processValue(value: unknown): void {
assertIsString(value);
// Ab hier weiß TypeScript: value ist string
console.log(value.toUpperCase());
}
function processUser(user: User | null): void {
assertIsDefined(user);
// Ab hier weiß TypeScript: user ist User (nicht null)
console.log(user.name);
}
Exhaustiveness Checking
Sicherstellen, dass alle Fälle behandelt werden:
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rectangle"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function getArea(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rectangle":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
default:
// Exhaustiveness check
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}
// Wenn du einen neuen Shape-Typ hinzufügst, zeigt TypeScript einen Fehler
// bis du den Fall im switch behandelst
Praktische Beispiele
API Response Handler
type ApiSuccess<T> = {
type: "success";
data: T;
statusCode: number;
};
type ApiError = {
type: "error";
message: string;
statusCode: number;
};
type ApiResponse<T> = ApiSuccess<T> | ApiError;
function isApiSuccess<T>(response: ApiResponse<T>): response is ApiSuccess<T> {
return response.type === "success";
}
async function fetchUser(id: number): Promise<ApiResponse<User>> {
// Implementation...
return { type: "success", data: { id, name: "Max", email: "max@example.com" }, statusCode: 200 };
}
async function handleUserFetch(id: number): Promise<void> {
const response = await fetchUser(id);
if (isApiSuccess(response)) {
console.log(`User: ${response.data.name}`);
} else {
console.error(`Error: ${response.message}`);
}
}
Form Validation
type ValidationSuccess = {
valid: true;
data: Record<string, string>;
};
type ValidationError = {
valid: false;
errors: string[];
};
type ValidationResult = ValidationSuccess | ValidationError;
function isValidationSuccess(result: ValidationResult): result is ValidationSuccess {
return result.valid;
}
function validateForm(form: Record<string, string>): ValidationResult {
const errors: string[] = [];
if (!form.email?.includes("@")) {
errors.push("Invalid email");
}
if (!form.password || form.password.length < 8) {
errors.push("Password must be at least 8 characters");
}
if (errors.length > 0) {
return { valid: false, errors };
}
return { valid: true, data: form };
}
function handleFormSubmit(form: Record<string, string>): void {
const result = validateForm(form);
if (isValidationSuccess(result)) {
console.log("Submitting:", result.data);
} else {
console.log("Validation errors:", result.errors);
}
}
Event System
type ClickEvent = {
type: "click";
x: number;
y: number;
};
type KeyboardEvent = {
type: "keyboard";
key: string;
keyCode: number;
};
type ScrollEvent = {
type: "scroll";
scrollTop: number;
};
type AppEvent = ClickEvent | KeyboardEvent | ScrollEvent;
function isClickEvent(event: AppEvent): event is ClickEvent {
return event.type === "click";
}
function isKeyboardEvent(event: AppEvent): event is KeyboardEvent {
return event.type === "keyboard";
}
function handleEvent(event: AppEvent): void {
if (isClickEvent(event)) {
console.log(`Click at (${event.x}, ${event.y})`);
} else if (isKeyboardEvent(event)) {
console.log(`Key pressed: ${event.key}`);
} else {
console.log(`Scrolled to: ${event.scrollTop}`);
}
}
Zusammenfassung
| Type Guard | Verwendung |
|---|---|
typeof | Primitive Typen |
instanceof | Klassen |
in | Property-Existenz |
| Truthiness | null/undefined Check |
| Discriminated Union | Objekte mit type/kind |
Custom (is) | Komplexe Typ-Prüfungen |
Assertion (asserts) | Throw bei falschem Typ |
Best Practices:
- Nutze Discriminated Unions für Objekt-Varianten
- Schreibe wiederverwendbare Custom Type Guards
- Verwende Exhaustiveness Checking für vollständige Abdeckung
- Assertion Functions für Validierung mit Exceptions
Im nächsten Kapitel lernst du Mapped Types!