Readonly Arrays und Tupel in TypeScript typsicher nutzen
AI generated
type
TypeScript
Readonly Arrays und Tupel: Unveränderlichkeit erzwingen
Mutationsfehler bereits zur Kompilierzeit statt erst im Produktivbetrieb erkennen

Arrays gelten in JavaScript standardmäßig als veränderlich, was in größeren Codebasen zu subtilen Fehlern führt, wenn eine Funktion ein übergebenes Array unbeabsichtigt verändert. TypeScript bietet mit readonly Arrays und readonly Tupeln ein Werkzeug, um solche Mutationen bereits zur Kompilierzeit auszuschließen.

9 Min. Lesezeit Typsystem Immutability

1. Das Problem stillschweigender Mutation

Wird ein Array an eine Funktion übergeben, kann diese Funktion es ohne jede Warnung verändern, etwa durch push, sort oder splice. Der Aufrufer merkt davon oft nichts, bis an anderer Stelle im Programm plötzlich unerwartete Werte auftauchen, weil das ursprüngliche Array in der Zwischenzeit verändert wurde.

Solche Fehler sind besonders tückisch, weil sie sich erst zur Laufzeit zeigen und oft weit entfernt von der eigentlichen Ursache. TypeScript kann dieses Problem bereits zur Kompilierzeit verhindern, wenn die Absicht der Unveränderlichkeit im Typsystem festgehalten wird.

Besonders in asynchronem Code verschärft sich das Problem: Wird ein Array an mehrere parallel laufende Funktionen weitergereicht, kann eine unbemerkte Mutation in einer Funktion zu Race Conditions führen, die sich nur schwer reproduzieren lassen und in Tests oft unentdeckt bleiben.

2. Grundlagen: readonly T[] und ReadonlyArray

Ein Array-Typ kann mit dem readonly Modifier versehen werden, entweder als readonly T[] oder äquivalent als ReadonlyArray<T>. Beide Schreibweisen sind funktional identisch, readonly T[] ist lediglich die kürzere Syntax.

Sobald ein Array als readonly typisiert ist, blendet der Compiler alle mutierenden Methoden wie push, pop, splice oder sort aus. Ein Zugriffsversuch auf eine dieser Methoden führt zu einem Kompilierfehler, lesende Methoden wie map, filter oder slice bleiben dagegen weiterhin verfügbar.

Der Editor unterstützt diese Prüfung bereits während des Tippens: Sobald eine Variable als readonly Array erkannt wird, listet die Autovervollständigung nur noch die tatsächlich verfügbaren, nicht mutierenden Methoden auf.


function printAll(items: readonly string[]) {
  console.log(items.join(", "));
  items.push("neu"); // Fehler: push existiert nicht auf readonly string[]
}

const list: ReadonlyArray<number> = [1, 2, 3];
const doubled = list.map((n) => n * 2); // erlaubt, erzeugt neues Array

3. Readonly Tupel

Auch Tupel, also Arrays mit fester Länge und festem Typ an jeder Position, lassen sich mit readonly versehen. Das ist besonders wichtig, da Tupel oft für Koordinaten, Bereichsangaben oder Rückgabewerte mit mehreren Werten verwendet werden, bei denen eine versehentliche Umsortierung oder Längenänderung fatale Folgen hätte.

Ohne readonly erlaubt TypeScript bei einem normalen Tupel trotz fester Länge weiterhin Methoden wie push, was die feste Struktur zur Laufzeit unterlaufen kann. Ein readonly Tupel verhindert das zuverlässig.

Gerade bei Funktionen, die mehrere Werte als Tupel zurückgeben, etwa ein Ergebnis zusammen mit einem Fehlerobjekt nach dem Vorbild von Go, sorgt readonly zusätzlich dafür, dass der Aufrufer die Reihenfolge der Rückgabewerte nicht versehentlich vertauscht oder verändert.


function distance(a: readonly [number, number], b: readonly [number, number]) {
  return Math.hypot(a[0] - b[0], a[1] - b[1]);
}

const origin: readonly [number, number] = [0, 0];
// origin.push(5); // Fehler: push existiert nicht auf readonly Tupel

4. Kombination mit as const und satisfies

as const erzeugt für Array-Literale automatisch einen readonly Tupel-Typ mit den engsten Literal-Typen jeder Position. Diese Kombination ist besonders wertvoll für Konstanten, die niemals verändert werden sollen und deren exakte Werte im Typsystem erhalten bleiben sollen.

In Kombination mit satisfies lässt sich zusätzlich prüfen, ob ein solches unveränderliches Array-Literal zu einem erwarteten Typ passt, ohne die durch as const gewonnene Präzision zu verlieren.

Dieses Muster ersetzt in vielen Projekten vollständig die Notwendigkeit eines Enums für feste Listen von Werten, da sowohl die Unveränderlichkeit als auch die exakten Literal-Typen bereits über die Kombination aus as const und dem daraus abgeleiteten Union-Typ abgedeckt werden.


const weekdays = ["Mo", "Di", "Mi", "Do", "Fr"] as const;
// Typ: readonly ["Mo", "Di", "Mi", "Do", "Fr"]

type Weekday = (typeof weekdays)[number];
// Typ: "Mo" | "Di" | "Mi" | "Do" | "Fr"

5. readonly als Vertrag für Funktionsparameter

Ein bewährtes Muster ist, Funktionsparameter grundsätzlich als readonly zu deklarieren, wenn die Funktion das übergebene Array nicht verändern soll. Das macht die Absicht der Funktion für Aufrufer sofort ersichtlich und verhindert versehentliche Mutation innerhalb der Funktion selbst.

Praktischerweise akzeptiert eine Funktion, die einen readonly Array-Parameter erwartet, auch ganz normale, veränderliche Arrays als Argument, da ein veränderliches Array immer alle Fähigkeiten eines unveränderlichen Arrays besitzt. Der umgekehrte Fall funktioniert dagegen nicht.

In React- und ähnlichen State-Management-Kontexten ist dieses Muster besonders wertvoll, da Zustandswerte grundsätzlich unveränderlich behandelt werden sollten. readonly Parameter erzwingen diese Konvention bereits zur Kompilierzeit, statt sich allein auf Disziplin im Team zu verlassen.


function sum(values: readonly number[]): number {
  return values.reduce((total, v) => total + v, 0);
}

const mutable = [1, 2, 3];
sum(mutable); // erlaubt, mutable[] ist kompatibel zu readonly number[]

6. Nur flache Unveränderlichkeit

Ein wichtiger Grenzfall: readonly wirkt nur auf der obersten Ebene. Ein readonly Array von veränderlichen Objekten verhindert zwar push oder splice auf dem Array selbst, hindert aber niemanden daran, eine Eigenschaft eines enthaltenen Objekts zu verändern.

Für echte, tiefe Unveränderlichkeit über mehrere Verschachtelungsebenen hinweg braucht es entweder verschachtelte readonly Modifier auf jedem Objekttyp oder einen rekursiven DeepReadonly Utility-Typ, den viele Projekte selbst definieren.


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

const points: readonly Point[] = [{ x: 0, y: 0 }];
points[0].x = 5; // erlaubt, da nur das Array selbst readonly ist
// points.push({ x: 1, y: 1 }); // Fehler

7. Vergleich zu Object.freeze zur Laufzeit

readonly ist ein reines Compile-Time-Feature ohne jede Laufzeit-Prüfung. Wird ein readonly typisiertes Array mit as any oder einer anderen Type Assertion umgangen, lässt sich der Inhalt trotzdem verändern, ohne dass zur Laufzeit ein Fehler auftritt.

Object.freeze bewirkt dagegen tatsächlichen Schutz zur Laufzeit: Ein Mutationsversuch schlägt im strict mode mit einer Exception fehl oder wird im nicht-strikten Modus stillschweigend ignoriert. Für maximale Sicherheit lassen sich beide Techniken kombinieren, readonly für die statische Prüfung während der Entwicklung, Object.freeze für den tatsächlichen Schutz zur Laufzeit.


const config = Object.freeze(["a", "b", "c"]) as readonly string[];
// Kompilierzeit-Schutz durch readonly
// Laufzeit-Schutz durch Object.freeze

8. Best Practices im Projektalltag

Für öffentliche Funktionssignaturen, insbesondere in Bibliotheken und geteilten Utility-Modulen, lohnt es sich, Array-Parameter standardmäßig als readonly zu deklarieren, sofern die Funktion sie nicht tatsächlich verändern muss. Das erhöht die Flexibilität für Aufrufer und dokumentiert gleichzeitig die Absicht.

Für exportierte Konstanten, insbesondere Konfigurationslisten oder Lookup-Tabellen, ist die Kombination aus as const und einem daraus abgeleiteten Union-Typ das robusteste Muster, da es sowohl Unveränderlichkeit als auch maximale Typpräzision liefert.

9. Fallstricke im Alltag

Ein häufiger Fehler ist die Annahme, ein readonly Array biete auch zur Laufzeit Schutz. Ohne zusätzliches Object.freeze lässt sich der zugrunde liegende Array-Wert weiterhin durch jeden Code verändern, der nicht durch den TypeScript-Compiler geprüft wird, etwa externe JavaScript-Bibliotheken oder Type Assertions.

Ein zweiter Stolperstein betrifft die Zuweisungsrichtung: Ein veränderliches Array kann problemlos einer readonly Variable zugewiesen werden, der umgekehrte Weg von readonly zu veränderlich erfordert dagegen eine explizite Type Assertion, da der Compiler diese Richtung als potenziell unsicher einstuft.

Ein dritter, seltener beachteter Punkt betrifft Bibliotheken mit eigenen, älteren Typdefinitionen, die Array-Parameter ohne readonly deklarieren, obwohl sie das Array intern nicht verändern. In solchen Fällen kann die Übergabe eines readonly Arrays trotz korrekten Laufzeitverhaltens einen Kompilierfehler auslösen, der nur durch eine lokale Type Assertion umgangen werden kann.

Ein letzter Hinweis betrifft Performance-kritischen Code: readonly selbst hat keinerlei Laufzeit-Kosten, da der Modifier vollständig beim Kompilieren entfernt wird. Bedenken bezüglich zusätzlichem Overhead durch die Verwendung von readonly sind daher in jedem Fall unbegründet und sollten kein Hindernis für die konsequente Nutzung darstellen.

Merkmal Normales Array readonly Array Object.freeze Array
Mutierende Methoden im Compiler sichtbar Ja Nein Ja, aber wirkungslos
Laufzeit-Schutz vor Mutation Nein Nein Ja
Kompilierzeit-Schutz vor Mutation Nein Ja Nein
Kompatibel als Argument für readonly-Parameter Ja Ja Ja
Tiefe der Unveränderlichkeit Keine Nur oberste Ebene Nur oberste Ebene

Mironsoft

TypeScript-Migration, Typsicherheit und Team-Onboarding

JavaScript-Codebasis ohne Typsicherheit, aber keine Zeit für eine Rundum-Migration?

Wir migrieren bestehende JavaScript-Projekte schrittweise zu TypeScript, richten strikte Compiler-Einstellungen sauber ein und bringen Teams mit Code-Reviews und Style-Guides auf denselben Typsicherheits-Stand.

Migrations-Fahrplan

Schrittweise JS-zu-TS-Migration ohne Big-Bang-Risiko planen und umsetzen.

Strict-Mode-Einführung

tsconfig.json, ESLint-Regeln und CI-Checks für dauerhafte Typsicherheit aufsetzen.

Team-Onboarding

Entwickler mit Workshops und Code-Reviews in TypeScript-Best-Practices einarbeiten.

10. Zusammenfassung

Readonly Arrays und Tupel

Schutzebene

Nur zur Kompilierzeit

Kombinierbar mit

as const, satisfies, Object.freeze

Tiefe

Standardmäßig nur flach

Empfohlen für

Funktionsparameter, Konstanten

11. FAQ: Readonly Arrays und Tupel

1Was bewirkt der readonly Modifier bei einem Array-Typ?
Er blendet alle mutierenden Methoden wie push, pop oder splice im Typsystem aus, sodass ein Zugriffsversuch auf diese Methoden bereits zur Kompilierzeit einen Fehler auslöst und nicht erst im Produktivbetrieb auffällt.
2Was ist der Unterschied zwischen readonly T[] und ReadonlyArray?
Beide sind funktional identisch, readonly T[] ist lediglich die kürzere Syntaxform von ReadonlyArray, beide erzeugen denselben Compiler-Effekt.
3Bietet ein readonly Array auch Schutz zur Laufzeit?
Nein, readonly ist ein reines Compile-Time-Feature. Zur Laufzeit lässt sich der Array-Inhalt weiterhin verändern, sofern der Zugriff nicht durch den TypeScript-Compiler geprüft wird, etwa über externe Bibliotheken.
4Wie erzeugt man einen readonly Tupel-Typ?
Entweder durch explizite Deklaration wie readonly [number, number] oder implizit durch Anhängen von as const an ein Array-Literal, was zusätzlich die engsten Literal-Typen jeder Position erhält.
5Wirkt readonly auch auf verschachtelte Objekte innerhalb eines Arrays?
Nein, readonly wirkt standardmäßig nur auf der obersten Ebene. Eigenschaften von Objekten innerhalb eines readonly Arrays bleiben veränderlich, sofern kein zusätzlicher readonly Modifier auf dem Objekttyp selbst gesetzt ist.
6Kann ein veränderliches Array einer readonly Variable zugewiesen werden?
Ja, das ist ohne weiteres möglich, da ein veränderliches Array alle Fähigkeiten eines readonly Arrays besitzt. Der umgekehrte Weg erfordert eine explizite Type Assertion, da der Compiler ihn als unsicher einstuft.
7Wie unterscheidet sich readonly von Object.freeze?
readonly wirkt ausschließlich zur Kompilierzeit im Typsystem, Object.freeze bewirkt tatsächlichen Schutz zur Laufzeit. Für maximale Sicherheit lassen sich beide Techniken kombinieren, ohne sich gegenseitig auszuschließen.
8Warum sollten Funktionsparameter oft als readonly deklariert werden?
Das dokumentiert die Absicht, dass die Funktion das übergebene Array nicht verändert, erhöht die Flexibilität für Aufrufer und verhindert versehentliche Mutation innerhalb der Funktion selbst.
9Wie erreicht man echte tiefe Unveränderlichkeit?
Entweder durch verschachtelte readonly Modifier auf jedem betroffenen Objekttyp oder durch einen selbst definierten rekursiven DeepReadonly Utility-Typ, kombiniert mit Object.freeze für den zusätzlichen Laufzeit-Schutz.
10Welches Muster eignet sich am besten für unveränderliche Konstanten?
Die Kombination aus as const für ein Array-Literal und einem daraus mittels typeof und Indexzugriff abgeleiteten Union-Typ liefert sowohl Unveränderlichkeit als auch maximale Typpräzision, ohne zusätzlichen Code.