TypeScript bietet ein leistungsstarkes Typsystem, aber Typen werden zur Laufzeit gelöscht. Type Guards schließen diese Lücke, indem sie Typen zur Laufzeit einschränken.
Was sind Type Guards?
Ein Type Guard ist eine Laufzeitprüfung, die den Typ einer Variable einschränkt.
// Without type guard — TypeScript only knows 'value' is string | number
function process(value: string | number) {
// value.toUpperCase(); // Error: Property 'toUpperCase' does not exist on type 'number'
// With type guard — TypeScript narrows the type
if (typeof value === 'string') {
console.log(value.toUpperCase()); // OK: value is string here
} else {
console.log(value.toFixed(2)); // OK: value is number here
}
}typeof Type Guards
Der typeof-Operator ist der einfachste Type Guard.
function formatValue(value: string | number | boolean): string {
if (typeof value === 'string') {
// TypeScript knows: value is string
return value.trim().toLowerCase();
}
if (typeof value === 'number') {
// TypeScript knows: value is number
return value.toLocaleString('en-US', { maximumFractionDigits: 2 });
}
// TypeScript knows: value is boolean
return value ? 'Yes' : 'No';
}
// typeof works for these primitive types:
// 'string' | 'number' | 'bigint' | 'boolean'
// 'symbol' | 'undefined' | 'object' | 'function'
// Caveat: typeof null === 'object'
function processNullable(value: string | null) {
if (typeof value === 'object') {
// value could be null here! typeof null === 'object'
}
if (value !== null && typeof value === 'object') {
// Safe: value is not null
}
}instanceof Type Guards
instanceof prüft, ob ein Objekt eine Klasseninstanz ist.
class HttpError extends Error {
constructor(public statusCode: number, message: string) {
super(message);
this.name = 'HttpError';
}
}
class ValidationError extends Error {
constructor(public field: string, message: string) {
super(message);
this.name = 'ValidationError';
}
}
function handleError(error: Error) {
if (error instanceof HttpError) {
// TypeScript knows: error is HttpError
console.log(`HTTP ${error.statusCode}: ${error.message}`);
if (error.statusCode === 401) {
redirectToLogin();
}
} else if (error instanceof ValidationError) {
// TypeScript knows: error is ValidationError
console.log(`Validation failed on field: ${error.field}`);
highlightField(error.field);
} else {
// TypeScript knows: error is Error
console.log(`Unexpected error: ${error.message}`);
}
}
// instanceof with Date
function formatDate(input: string | Date): string {
if (input instanceof Date) {
return input.toISOString();
}
return new Date(input).toISOString();
}"in" Operator Type Guard
Der in-Operator prüft die Existenz einer Eigenschaft.
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
function move(animal: Bird | Fish) {
if ('fly' in animal) {
// TypeScript knows: animal is Bird
animal.fly();
} else {
// TypeScript knows: animal is Fish
animal.swim();
}
}
// Practical example: API responses
interface SuccessResponse {
data: unknown;
status: 'ok';
}
interface ErrorResponse {
error: string;
code: number;
}
function handleResponse(res: SuccessResponse | ErrorResponse) {
if ('error' in res) {
// TypeScript knows: res is ErrorResponse
console.error(`Error ${res.code}: ${res.error}`);
} else {
// TypeScript knows: res is SuccessResponse
processData(res.data);
}
}Benutzerdefinierte Type Guards
Funktionen mit Typprädikat kapseln komplexe Logik.
// Type predicate syntax: paramName is Type
interface User {
id: string;
name: string;
email: string;
}
interface Admin extends User {
role: 'admin';
permissions: string[];
}
// Custom type guard function
function isAdmin(user: User): user is Admin {
return 'role' in user && (user as Admin).role === 'admin';
}
function showDashboard(user: User) {
if (isAdmin(user)) {
// TypeScript knows: user is Admin
console.log(`Admin: ${user.name}, Permissions: ${user.permissions.join(', ')}`);
renderAdminPanel(user.permissions);
} else {
// TypeScript knows: user is User (not Admin)
console.log(`User: ${user.name}`);
renderUserDashboard();
}
}
// Type guard for checking non-null
function isNotNull<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined;
}
// Use with .filter() to narrow array types
const items: (string | null)[] = ['hello', null, 'world', null];
const strings: string[] = items.filter(isNotNull);
// strings is string[] — no more nulls!
// Type guard for object shapes
function isValidUser(data: unknown): data is User {
return (
typeof data === 'object' &&
data !== null &&
'id' in data &&
'name' in data &&
'email' in data &&
typeof (data as User).id === 'string' &&
typeof (data as User).name === 'string' &&
typeof (data as User).email === 'string'
);
}Diskriminierte Unions
Verwenden eine gemeinsame Literal-Eigenschaft zur Unterscheidung.
// The discriminant property: 'type'
interface Circle {
type: 'circle';
radius: number;
}
interface Rectangle {
type: 'rectangle';
width: number;
height: number;
}
interface Triangle {
type: 'triangle';
base: number;
height: number;
}
type Shape = Circle | Rectangle | Triangle;
function calculateArea(shape: Shape): number {
switch (shape.type) {
case 'circle':
// TypeScript knows: shape is Circle
return Math.PI * shape.radius ** 2;
case 'rectangle':
// TypeScript knows: shape is Rectangle
return shape.width * shape.height;
case 'triangle':
// TypeScript knows: shape is Triangle
return (shape.base * shape.height) / 2;
}
}
// Real-world: Redux actions
type Action =
| { type: 'FETCH_START' }
| { type: 'FETCH_SUCCESS'; payload: User[] }
| { type: 'FETCH_ERROR'; error: string }
| { type: 'SET_FILTER'; filter: string };
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'FETCH_START':
return { ...state, loading: true };
case 'FETCH_SUCCESS':
// TypeScript knows: action.payload exists and is User[]
return { ...state, loading: false, users: action.payload };
case 'FETCH_ERROR':
// TypeScript knows: action.error exists and is string
return { ...state, loading: false, error: action.error };
case 'SET_FILTER':
return { ...state, filter: action.filter };
}
}Assertionsfunktionen
Schränken Typen durch Werfen eines Fehlers ein.
// Assertion function syntax: asserts paramName is Type
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') {
throw new TypeError(`Expected string, got ${typeof value}`);
}
}
// Usage: type narrows AFTER the assertion call
function processInput(input: unknown) {
assertIsString(input);
// TypeScript knows: input is string from here on
console.log(input.toUpperCase());
}
// Assert non-null
function assertDefined<T>(
value: T | null | undefined,
name: string
): asserts value is T {
if (value === null || value === undefined) {
throw new Error(`${name} must be defined`);
}
}
// Practical: Form validation
interface FormData {
name: unknown;
email: unknown;
age: unknown;
}
function assertValidForm(data: FormData): asserts data is {
name: string;
email: string;
age: number;
} {
if (typeof data.name !== 'string' || data.name.length === 0) {
throw new ValidationError('name', 'Name is required');
}
if (typeof data.email !== 'string' || !data.email.includes('@')) {
throw new ValidationError('email', 'Valid email is required');
}
if (typeof data.age !== 'number' || data.age < 0 || data.age > 150) {
throw new ValidationError('age', 'Valid age is required');
}
}
function handleSubmit(data: FormData) {
assertValidForm(data);
// TypeScript knows all fields are properly typed here
console.log(`Name: ${data.name}, Email: ${data.email}, Age: ${data.age}`);
}Typ-Einschränkung durch Kontrollfluss
TypeScript analysiert den Kontrollfluss zur Typeinschränkung.
Wahrheitswert-Einschränkung
Schränkt Typen basierend auf Wahrheitswert-Prüfungen ein.
function greet(name: string | null | undefined) {
if (name) {
// TypeScript knows: name is string (not null/undefined)
console.log(`Hello, ${name.toUpperCase()}!`);
}
}
// Truthy narrowing with arrays
function processItems(items?: string[]) {
if (items && items.length > 0) {
// TypeScript knows: items is string[] (defined and non-empty)
const first = items[0]; // string
}
}
// Logical operators for narrowing
function getValue(a: string | null, b: string | null): string {
// || narrows: returns first truthy value
return a || b || 'default';
}
// Nullish coalescing for narrowing
function getConfig(config?: { timeout: number }) {
const timeout = config?.timeout ?? 3000;
// timeout is number (never undefined)
}Gleichheits-Einschränkung
Schränkt Typen bei Wertevergleichen ein.
function example(x: string | number, y: string | boolean) {
if (x === y) {
// TypeScript knows: both x and y are string
// (the only common type)
console.log(x.toUpperCase());
console.log(y.toUpperCase());
}
}
// Equality narrowing with null
function process(value: string | null) {
if (value !== null) {
// TypeScript knows: value is string
console.log(value.length);
}
}
// Switch statement narrowing
function handleStatus(status: 'loading' | 'success' | 'error') {
switch (status) {
case 'loading':
showSpinner();
break;
case 'success':
hideSpinner();
break;
case 'error':
showError();
break;
}
}Erweiterte Muster
Vollständigkeitsprüfung mit never
Stellt sicher, dass alle Fälle behandelt werden.
type Shape = Circle | Rectangle | Triangle;
function assertNever(x: never): never {
throw new Error(`Unexpected value: ${x}`);
}
function getArea(shape: Shape): number {
switch (shape.type) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rectangle':
return shape.width * shape.height;
case 'triangle':
return (shape.base * shape.height) / 2;
default:
// If you add a new Shape variant and forget to handle it,
// TypeScript will error here because shape won't be 'never'
return assertNever(shape);
}
}
// Alternative: satisfies + exhaustive check
function describeShape(shape: Shape): string {
switch (shape.type) {
case 'circle': return `Circle with radius ${shape.radius}`;
case 'rectangle': return `${shape.width}x${shape.height} rectangle`;
case 'triangle': return `Triangle with base ${shape.base}`;
default: {
const _exhaustive: never = shape;
return _exhaustive;
}
}
}Generische Type Guards
Erstellen Sie wiederverwendbare generische Type Guards.
// Generic type guard for checking object keys
function hasProperty<K extends string>(
obj: unknown,
key: K
): obj is Record<K, unknown> {
return typeof obj === 'object' && obj !== null && key in obj;
}
// Usage
function processData(data: unknown) {
if (hasProperty(data, 'name') && hasProperty(data, 'age')) {
console.log(data.name, data.age); // both are 'unknown' but accessible
}
}
// Generic type guard for arrays
function isArrayOf<T>(
arr: unknown,
guard: (item: unknown) => item is T
): arr is T[] {
return Array.isArray(arr) && arr.every(guard);
}
const isString = (value: unknown): value is string =>
typeof value === 'string';
function handleInput(data: unknown) {
if (isArrayOf(data, isString)) {
// data is string[]
data.forEach(s => console.log(s.toUpperCase()));
}
}Array Type Guards
Verwenden Sie Array.isArray() und typsicheres Filtern.
// Filtering with type guards
const mixed: (string | number | null)[] = ['a', 1, null, 'b', 2, null];
// Filter nulls with type predicate
const nonNull = mixed.filter(
(item): item is string | number => item !== null
);
// nonNull: (string | number)[]
// Filter to specific type
const stringsOnly = mixed.filter(
(item): item is string => typeof item === 'string'
);
// stringsOnly: string[]
// Type-safe .find()
const found = mixed.find(
(item): item is string => typeof item === 'string'
);
// found: string | undefined
// Array.isArray() type guard
function flatten(input: string | string[]): string[] {
if (Array.isArray(input)) {
return input; // string[]
}
return [input]; // wrap single string in array
}Praxisbeispiele
API-Antwortverarbeitung
Unverzichtbar für die Validierung von API-Antworten.
// API response types
interface ApiSuccess<T> {
success: true;
data: T;
}
interface ApiError {
success: false;
error: { message: string; code: string };
}
type ApiResponse<T> = ApiSuccess<T> | ApiError;
// Type guard based on discriminant
function isApiSuccess<T>(res: ApiResponse<T>): res is ApiSuccess<T> {
return res.success === true;
}
// Usage with fetch
async function fetchUser(id: string): Promise<User> {
const response = await fetch(`/api/users/${id}`);
const json: ApiResponse<User> = await response.json();
if (isApiSuccess(json)) {
return json.data; // TypeScript knows: json.data is User
} else {
throw new Error(json.error.message);
}
}
// Validate unknown API data
function isUser(data: unknown): data is User {
if (typeof data !== 'object' || data === null) return false;
const obj = data as Record<string, unknown>;
return (
typeof obj.id === 'string' &&
typeof obj.name === 'string' &&
typeof obj.email === 'string'
);
}Event-Behandlung
DOM-Eventtypen einschränken.
function handleEvent(event: Event) {
if (event instanceof MouseEvent) {
console.log(`Mouse at (${event.clientX}, ${event.clientY})`);
} else if (event instanceof KeyboardEvent) {
console.log(`Key pressed: ${event.key}`);
} else if (event instanceof TouchEvent) {
console.log(`Touch points: ${event.touches.length}`);
}
}
// Input element type narrowing
function handleInput(event: Event) {
const target = event.target;
if (target instanceof HTMLInputElement) {
console.log(target.value); // string
} else if (target instanceof HTMLSelectElement) {
console.log(target.selectedIndex); // number
}
}Fehlerbehandlung
Verschiedene Fehlertypen typsicher behandeln.
// Type guard for unknown caught errors
function isError(error: unknown): error is Error {
return error instanceof Error;
}
function getErrorMessage(error: unknown): string {
if (isError(error)) return error.message;
if (typeof error === 'string') return error;
return 'An unknown error occurred';
}
// Safe error handling in async code
async function safeFetch(url: string) {
try {
const res = await fetch(url);
if (!res.ok) throw new HttpError(res.status, res.statusText);
return await res.json();
} catch (error) {
if (error instanceof HttpError) {
handleHttpError(error); // error.statusCode available
} else if (error instanceof TypeError) {
handleNetworkError(error); // Network failure
} else {
handleUnknownError(getErrorMessage(error));
}
}
}Best Practices
- Bevorzugen Sie diskriminierte Unions.
- Halten Sie Type Guards einfach.
- Assertionsfunktionen an App-Grenzen.
- Behandeln Sie immer den else-Fall.
- Array.isArray() bei Union-Typen.
- Type Guards kombinieren.
- Benutzerdefinierte Type Guards testen.
Fazit
TypeScript Type Guards sind wesentliche Werkzeuge. Meistern Sie diese Muster für typsicheren und robusten Code.
FAQ
Unterschied zwischen Typprädikat und Assertionsfunktion?
Typprädikate geben boolean zurück, Assertionen werfen Fehler.
Wann diskriminierte Unions vs Type Guards?
Diskriminierte Unions wenn Sie die Typen kontrollieren, Type Guards für externe Daten.
Können Type Guards Laufzeitfehler verursachen?
Ja, wenn die Prädikatlogik nicht zum deklarierten Typ passt.
Wie Typen in Arrays einschränken?
Array.isArray() und .filter() mit Typprädikat verwenden.