TypeScript - Mapped Types: Ein Anfängerguide

Hallo da draußen, zukünftige TypeScript-Zauberer! Heute begeben wir uns auf eine aufregende Reise in die Welt der Mapped Types. Keine Sorge, wenn du neu im Programmieren bist – ich werde dein freundlicher Guide sein, und wir gehen das Schritt für Schritt durch. Bis zum Ende dieses Tutorials wirst du Typen wie ein Profi mappen können!

TypeScript - Mapped Types

Was sind Mapped Types?

Bevor wir tiefer einsteigen, lassen Sie uns verstehen, was Mapped Types sind. Stell dir vor, du hast eine Schachtel mit gemischten Pralinen und du möchtest eine neue Schachtel erstellen, in der jede Praline in Goldfolie eingewickelt ist. Das ist im Wesentlichen, was Mapped Types in TypeScript tun – sie nehmen einen existierenden Typ und transformieren ihn in einen neuen Typ basierend auf einem Satz von Regeln.

Eingebaute Mapped Types

TypeScript bringt einige vordefinierte Mapped Types mit, die äußerst nützlich sind. Lassen Sie uns sie einzeln anschauen:

1. Partial

Der Typ Partial<T> macht alle Eigenschaften von T optional. Es ist so, als ob man ein strenges Rezept in ein flexibles umwandelt, bei dem man einige Zutaten weglassen kann.

interface Recipe {
name: string;
ingredients: string[];
cookingTime: number;
}

type FlexibleRecipe = Partial<Recipe>;

// Jetzt können wir ein Rezept ohne alle Eigenschaften erstellen
const quickSnack: FlexibleRecipe = {
name: "Toast",
// Wir können Zutaten und Kochzeit überspringen
};

In diesem Beispiel ermöglicht FlexibleRecipe, dass wir ein Rezept ohne alle Eigenschaften erstellen. Es ist perfekt, wenn man nur einen Teil eines Objekts aktualisieren möchte.

2. Required

Required<T> ist das Gegenteil von Partial<T>. Es macht alle Eigenschaften obligatorisch, selbst wenn sie im ursprünglichen Typ optional waren.

interface UserProfile {
name: string;
age?: number;
email?: string;
}

type CompleteUserProfile = Required<UserProfile>;

// Jetzt müssen wir alle Eigenschaften angeben
const user: CompleteUserProfile = {
name: "Alice",
age: 30,
email: "[email protected]"
};

Hier stellt CompleteUserProfile sicher, dass wir alle Eigenschaften angeben, einschließlich age und email, die im ursprünglichen UserProfile optional waren.

3. Readonly

Readonly<T> macht alle Eigenschaften von T schreibgeschützt. Es ist so, als ob man seinen Typ in ein Glasdisplay stellt – man kann hinschauen, aber nicht anfassen!

interface Toy {
name: string;
price: number;
}

type CollectibleToy = Readonly<Toy>;

const actionFigure: CollectibleToy = {
name: "Superheld",
price: 19.99
};

// Dies verursacht einen Fehler
// actionFigure.price = 29.99;

In diesem Beispiel können wir die Eigenschaften unseres actionFigure nach der Erstellung nicht mehr ändern. Es ist großartig für die Erstellung unveränderlicher Objekte.

4. Pick<T, K>

Pick<T, K> erstellt einen neuen Typ, indem es nur die angegebenen Eigenschaften K von T auswählt. Es ist so, als würde man seine Lieblingsmerkmale von einem Typ auswählen.

interface Smartphone {
brand: string;
model: string;
year: number;
color: string;
price: number;
}

type BasicPhoneInfo = Pick<Smartphone, 'brand' | 'model'>;

const myPhone: BasicPhoneInfo = {
brand: "TechBrand",
model: "X2000"
};

Hier enthält BasicPhoneInfo nur die Eigenschaften brand und model von Smartphone, während der Rest weggelassen wird.

5. Omit<T, K>

Omit<T, K> ist das Gegenteil von Pick<T, K>. Es erstellt einen neuen Typ, indem es die angegebenen Eigenschaften K von T entfernt.

interface Book {
title: string;
author: string;
pages: number;
isbn: string;
}

type BookPreview = Omit<Book, 'pages' | 'isbn'>;

const preview: BookPreview = {
title: "TypeScript Abenteuer",
author: "Code-Zauberer"
};

In diesem Fall enthält BookPreview alle Eigenschaften von Book, außer pages und isbn.

Beispiele für die Verwendung von Mapped Types

Nun, da wir die eingebauten Mapped Types gesehen haben, lassen uns einige praktische Beispiele anschauen, wie wir sie in realen Szenarien verwenden können.

Beispiel 1: Erstellen eines Formularzustands

Stellen wir uns vor, du baust ein Formular und möchtest nachverfolgen, welche Felder geändert wurden:

interface LoginForm {
username: string;
password: string;
rememberMe: boolean;
}

type FormTouched = { [K in keyof LoginForm]: boolean };

const touchedFields: FormTouched = {
username: true,
password: false,
rememberMe: true
};

Hier haben wir einen FormTouched-Typ erstellt, der die gleichen Schlüssel wie LoginForm hat, aber alle Werte Booleans sind, die angeben, ob das Feld berührt wurde.

Beispiel 2: API-Antwort-Wrapper

Angenommen, du hast eine API, die verschiedene Typen von Daten zurückgibt, und du möchtest jede Antwort in ein standardisiertes Format einpacken:

interface ApiResponse<T> {
data: T;
status: 'success' | 'error';
timestamp: number;
}

type UserData = { id: number; name: string; email: string };
type ProductData = { id: number; name: string; price: number };

const userResponse: ApiResponse<UserData> = {
data: { id: 1, name: "John Doe", email: "[email protected]" },
status: 'success',
timestamp: Date.now()
};

const productResponse: ApiResponse<ProductData> = {
data: { id: 101, name: "Laptop", price: 999.99 },
status: 'success',
timestamp: Date.now()
};

Dieses Beispiel zeigt, wie wir mit Generics und Mapped Types flexible, wiederverwendbare Typenstrukturen erstellen können.

Erstellen benutzerdefinierter Mapped Types

Nun, lassen uns unsere TypeScript-Muskeln spielen lassen und einige benutzerdefinierte Mapped Types erstellen!

Benutzerdefinierter Typ 1: Nullable

Lassen wir uns einen Typ erstellen, der alle Eigenschaften nullable macht:

type Nullable<T> = { [K in keyof T]: T[K] | null };

interface Person {
name: string;
age: number;
}

type NullablePerson = Nullable<Person>;

const maybePerson: NullablePerson = {
name: "Jane",
age: null  // Dies ist jetzt gültig
};

Unser Nullable<T>-Typ ermöglicht es jeder Eigenschaft, entweder ihren ursprünglichen Typ oder null zu sein.

Benutzerdefinierter Typ 2: Freezable

Lassen wir uns einen Typ erstellen, der einer Anwendung eine freeze-Methode hinzufügt:

type Freezable<T> = T & { freeze(): Readonly<T> };

interface Config {
theme: string;
fontSize: number;
}

function makeFreezable<T>(obj: T): Freezable<T> {
return {
...obj,
freeze() {
return Object.freeze({ ...this }) as Readonly<T>;
}
};
}

const config = makeFreezable<Config>({
theme: "dunkel",
fontSize: 14
});

const frozenConfig = config.freeze();
// frozenConfig.theme = "hell";  // Dies würde einen Fehler verursachen

Dieser benutzerdefinierte Typ fügt eine freeze-Methode zu jedem Objekt hinzu, die eine unveränderliche Version davon erstellt.

Fazit

Wow, wir haben heute viel Boden cobered! Von eingebauten Mapped Types bis hin zur Erstellung eigener benutzerdefinierter Typen, du hast gesehen, wie mächtig und flexibel TypeScript sein kann. Mapped Types sind wie magische Zauberstäbe in deinem TypeScript-Werkzeugkasten – sie ermöglichen es dir, Typen auf unglaublich nützliche Weise zu transformieren und zu manipulieren.

Denk daran, der Schlüssel zum Beherrschen von Mapped Types ist Übung. Versuche, deine eigenen zu erstellen, experimentiere mit verschiedenen Kombinationen, und bald wirst du TypeScript-Code schreiben, der nicht nur funktional, sondern auch elegant und typensicher ist.

Weiter codieren, weiter lernen und vor allem Spaß mit TypeScript haben!

Credits: Image by storyset