Deutsch

Nutzen Sie die Macht unveränderlicher Datenstrukturen in TypeScript mit Readonly-Typen. Erstellen Sie vorhersagbare, wartbare und robuste Anwendungen, indem Sie unbeabsichtigte Datenmutationen verhindern.

TypeScript Readonly-Typen: Unveränderliche Datenstrukturen meistern

In der sich ständig weiterentwickelnden Landschaft der Softwareentwicklung ist das Streben nach robustem, vorhersagbarem und wartbarem Code ein konstantes Unterfangen. TypeScript bietet mit seinem starken Typsystem leistungsstarke Werkzeuge, um diese Ziele zu erreichen. Unter diesen Werkzeugen ragen Readonly-Typen als entscheidender Mechanismus zur Durchsetzung von Immutabilität hervor – ein Eckpfeiler der funktionalen Programmierung und ein Schlüssel zum Erstellen zuverlässigerer Anwendungen.

Was ist Immutabilität und warum ist sie wichtig?

Immutabilität bedeutet im Kern, dass der Zustand eines Objekts nach seiner Erstellung nicht mehr geändert werden kann. Dieses einfache Konzept hat tiefgreifende Auswirkungen auf die Codequalität und Wartbarkeit.

Readonly-Typen in TypeScript: Ihr Immutabilitäts-Arsenal

TypeScript bietet verschiedene Möglichkeiten, Immutabilität mit dem Schlüsselwort readonly durchzusetzen. Lassen Sie uns die verschiedenen Techniken und ihre praktische Anwendung untersuchen.

1. Readonly-Eigenschaften bei Interfaces und Typen

Der einfachste Weg, eine Eigenschaft als readonly zu deklarieren, ist die direkte Verwendung des Schlüsselworts readonly in einer Interface- oder Typdefinition.


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

const person: Person = {
  id: "unique-id-123",
  name: "Alice",
  age: 30,
};

// person.id = "new-id"; // Fehler: 'id' kann nicht zugewiesen werden, da es eine schreibgeschützte Eigenschaft ist.
person.name = "Bob"; // Dies ist erlaubt

In diesem Beispiel wird die Eigenschaft id als readonly deklariert. TypeScript verhindert jeden Versuch, sie nach der Erstellung des Objekts zu ändern. Die Eigenschaften name und age, denen der readonly-Modifikator fehlt, können frei geändert werden.

2. Der Readonly-Utility-Typ

TypeScript bietet einen leistungsstarken Utility-Typ namens Readonly<T>. Dieser generische Typ nimmt einen bestehenden Typ T und transformiert ihn, indem er alle seine Eigenschaften zu readonly macht.


interface Point {
  x: number;
  y: number;
}

const point: Readonly<Point> = {
  x: 10,
  y: 20,
};

// point.x = 30; // Fehler: 'x' kann nicht zugewiesen werden, da es eine schreibgeschützte Eigenschaft ist.

Der Typ Readonly<Point> erstellt einen neuen Typ, bei dem sowohl x als auch y readonly sind. Dies ist eine bequeme Möglichkeit, einen bestehenden Typ schnell unveränderlich zu machen.

3. Readonly-Arrays (ReadonlyArray<T>) und readonly T[]

Arrays in JavaScript sind von Natur aus veränderlich. TypeScript bietet eine Möglichkeit, schreibgeschützte Arrays mit dem Typ ReadonlyArray<T> oder der Kurzschreibweise readonly T[] zu erstellen. Dies verhindert die Änderung des Array-Inhalts.


const numbers: ReadonlyArray<number> = [1, 2, 3, 4, 5];
// numbers.push(6); // Fehler: Eigenschaft 'push' existiert nicht für den Typ 'readonly number[]'.
// numbers[0] = 10; // Fehler: Die Indexsignatur im Typ 'readonly number[]' erlaubt nur das Lesen.

const moreNumbers: readonly number[] = [6, 7, 8, 9, 10]; // Äquivalent zu ReadonlyArray
// moreNumbers.push(11); // Fehler: Eigenschaft 'push' existiert nicht für den Typ 'readonly number[]'.

Der Versuch, Methoden zu verwenden, die das Array ändern, wie push, pop, splice, oder direkt einem Index zuzuweisen, führt zu einem TypeScript-Fehler.

4. const vs. readonly: Den Unterschied verstehen

Es ist wichtig, zwischen const und readonly zu unterscheiden. const verhindert die Neuzuweisung der Variable selbst, während readonly die Änderung der Eigenschaften des Objekts verhindert. Sie dienen unterschiedlichen Zwecken und können für maximale Immutabilität zusammen verwendet werden.


const immutableNumber = 42;
// immutableNumber = 43; // Fehler: Neuzuweisung zu const-Variable 'immutableNumber' nicht möglich.

const mutableObject = { value: 10 };
mutableObject.value = 20; // Dies ist erlaubt, da das *Objekt* nicht const ist, sondern nur die Variable.

const readonlyObject: Readonly<{ value: number }> = { value: 30 };
// readonlyObject.value = 40; // Fehler: 'value' kann nicht zugewiesen werden, da es eine schreibgeschützte Eigenschaft ist.

const constReadonlyObject: Readonly<{ value: number }> = { value: 50 };
// constReadonlyObject = { value: 60 }; // Fehler: Neuzuweisung zu const-Variable 'constReadonlyObject' nicht möglich.
// constReadonlyObject.value = 60; // Fehler: 'value' kann nicht zugewiesen werden, da es eine schreibgeschützte Eigenschaft ist.

Wie oben gezeigt, stellt const sicher, dass die Variable immer auf dasselbe Objekt im Speicher zeigt, während readonly garantiert, dass der interne Zustand des Objekts unverändert bleibt.

Praktische Beispiele: Anwendung von Readonly-Typen in realen Szenarien

Lassen Sie uns einige praktische Beispiele untersuchen, wie Readonly-Typen verwendet werden können, um die Codequalität und Wartbarkeit in verschiedenen Szenarien zu verbessern.

1. Verwalten von Konfigurationsdaten

Konfigurationsdaten werden oft einmal beim Start der Anwendung geladen und sollten während der Laufzeit nicht geändert werden. Die Verwendung von Readonly-Typen stellt sicher, dass diese Daten konsistent bleiben und verhindert versehentliche Änderungen.


interface AppConfig {
  readonly apiUrl: string;
  readonly timeout: number;
  readonly features: readonly string[];
}

const config: AppConfig = {
  apiUrl: "https://api.example.com",
  timeout: 5000,
  features: ["featureA", "featureB"],
};

function fetchData(url: string, config: Readonly<AppConfig>) {
    // ... config.timeout und config.apiUrl sicher verwenden, da sie sich nicht ändern werden
}

fetchData("/data", config);

2. Implementierung eines Redux-ähnlichen Zustandsmanagements

In Zustandsverwaltungsbibliotheken wie Redux ist Immutabilität ein Kernprinzip. Readonly-Typen können verwendet werden, um sicherzustellen, dass der Zustand unveränderlich bleibt und dass Reducer nur neue Zustandsobjekte zurückgeben, anstatt die vorhandenen zu ändern.


interface State {
  readonly count: number;
  readonly items: readonly string[];
}

const initialState: State = {
  count: 0,
  items: [],
};

function reducer(state: Readonly<State>, action: { type: string; payload?: any }): State {
  switch (action.type) {
    case "INCREMENT":
      return { ...state, count: state.count + 1 }; // Ein neues Zustandsobjekt zurückgeben
    case "ADD_ITEM":
      return { ...state, items: [...state.items, action.payload] }; // Ein neues Zustandsobjekt mit aktualisierten Elementen zurückgeben
    default:
      return state;
  }
}

3. Arbeiten mit API-Antworten

Beim Abrufen von Daten von einer API ist es oft wünschenswert, die Antwortdaten als unveränderlich zu behandeln, insbesondere wenn Sie sie zum Rendern von UI-Komponenten verwenden. Readonly-Typen können helfen, versehentliche Mutationen der API-Daten zu verhindern.


interface ApiResponse {
  readonly userId: number;
  readonly id: number;
  readonly title: string;
  readonly completed: boolean;
}

async function fetchTodo(id: number): Promise<Readonly<ApiResponse>> {
  const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
  const data: ApiResponse = await response.json();
  return data;
}

fetchTodo(1).then(todo => {
  console.log(todo.title);
  // todo.completed = true; // Fehler: 'completed' kann nicht zugewiesen werden, da es eine schreibgeschützte Eigenschaft ist.
});

4. Modellierung geografischer Daten (Internationales Beispiel)

Stellen Sie sich die Darstellung geografischer Koordinaten vor. Sobald eine Koordinate festgelegt ist, sollte sie idealerweise konstant bleiben. Dies gewährleistet die Datenintegrität, insbesondere bei sensiblen Anwendungen wie Kartierungs- oder Navigationssystemen, die in verschiedenen geografischen Regionen betrieben werden (z. B. GPS-Koordinaten für einen Lieferservice, der Nordamerika, Europa und Asien abdeckt).


interface GeoCoordinates {
 readonly latitude: number;
 readonly longitude: number;
}

const tokyoCoordinates: GeoCoordinates = {
 latitude: 35.6895,
 longitude: 139.6917
};

const newYorkCoordinates: GeoCoordinates = {
 latitude: 40.7128,
 longitude: -74.0060
};


function calculateDistance(coord1: Readonly<GeoCoordinates>, coord2: Readonly<GeoCoordinates>): number {
 // Stellen Sie sich eine komplexe Berechnung mit Breiten- und Längengrad vor
 // Rückgabe eines Platzhalterwertes zur Vereinfachung
 return 1000; 
}

const distance = calculateDistance(tokyoCoordinates, newYorkCoordinates);
console.log("Abstand zwischen Tokio und New York (Platzhalter):", distance);

// tokyoCoordinates.latitude = 36.0; // Fehler: 'latitude' kann nicht zugewiesen werden, da es eine schreibgeschützte Eigenschaft ist.

Tiefgreifend schreibgeschützte Typen: Umgang mit verschachtelten Objekten

Der Utility-Typ Readonly<T> macht nur die direkten Eigenschaften eines Objekts readonly. Wenn ein Objekt verschachtelte Objekte oder Arrays enthält, bleiben diese verschachtelten Strukturen veränderlich. Um eine wirklich tiefe Immutabilität zu erreichen, müssen Sie Readonly<T> rekursiv auf alle verschachtelten Eigenschaften anwenden.

Hier ist ein Beispiel, wie man einen tiefgreifend schreibgeschützten Typ erstellt:


type DeepReadonly<T> = T extends (infer R)[]
  ? DeepReadonlyArray<R>
  : T extends object
  ? DeepReadonlyObject<T>
  : T;

interface DeepReadonlyArray<T> extends ReadonlyArray<DeepReadonly<T>> {}

type DeepReadonlyObject<T> = {
  readonly [P in keyof T]: DeepReadonly<T[P]>;
};

interface Company {
  name: string;
  address: {
    street: string;
    city: string;
    country: string;
  };
  employees: string[];
}

const company: DeepReadonly<Company> = {
  name: "Example Corp",
  address: {
    street: "123 Main St",
    city: "Anytown",
    country: "USA",
  },
  employees: ["Alice", "Bob"],
};

// company.name = "New Corp"; // Fehler
// company.address.city = "New City"; // Fehler
// company.employees.push("Charlie"); // Fehler

Dieser DeepReadonly<T>-Typ wendet Readonly<T> rekursiv auf alle verschachtelten Eigenschaften an und stellt sicher, dass die gesamte Objektstruktur unveränderlich ist.

Überlegungen und Kompromisse

Obwohl Immutabilität erhebliche Vorteile bietet, ist es wichtig, sich der potenziellen Kompromisse bewusst zu sein.

Bibliotheken für unveränderliche Datenstrukturen

Mehrere Bibliotheken können die Arbeit mit unveränderlichen Datenstrukturen in TypeScript vereinfachen:

Best Practices für die Verwendung von Readonly-Typen

Um Readonly-Typen in Ihren TypeScript-Projekten effektiv zu nutzen, befolgen Sie diese Best Practices:

Fazit: Immutabilität mit TypeScript Readonly-Typen annehmen

Die Readonly-Typen von TypeScript sind ein leistungsstarkes Werkzeug zum Erstellen vorhersagbarerer, wartbarerer und robusterer Anwendungen. Indem Sie die Immutabilität annehmen, können Sie das Fehlerrisiko reduzieren, das Debugging vereinfachen und die Gesamtqualität Ihres Codes verbessern. Obwohl es einige Kompromisse zu berücksichtigen gibt, überwiegen die Vorteile der Immutabilität oft die Kosten, insbesondere bei komplexen und langlebigen Projekten. Machen Sie Readonly-Typen auf Ihrer weiteren TypeScript-Reise zu einem zentralen Bestandteil Ihres Entwicklungs-Workflows, um das volle Potenzial der Immutabilität auszuschöpfen und wirklich zuverlässige Software zu erstellen.