Zum Inhalt springen
TypeScript Fortgeschritten 30 min

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 GuardVerwendung
typeofPrimitive Typen
instanceofKlassen
inProperty-Existenz
Truthinessnull/undefined Check
Discriminated UnionObjekte 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!

Zurück zum TypeScript Kurs