LEAN-CODERS Logo

Blog

LEAN-CODERS

TypeScript: const vs readonly - weniger Fehler durch defensive Programmierung

Coffee mug

"const" Arrays und Objekte sind, entgegen der Intuition, veränderbar in JavaScript und TypeScript.

... und in diesem Artikel erfährst du, warum, und wie const, as const und readonly in der Praxis anwendbar sind. Wir kratzen das Thema nicht nur oberflächlich an, sondern ergründen die Eigenschaften und Unterschiede dieser Keywords direkt im Herzen TypeScripts: Im Compiler und dem Typsystem - los geht's!

Welche Rolle spielt der TypeScript Compiler?

Um genau zu verstehen, was const, as const und readonly tun, müssen wir verstehen, wie der TypeScript Compiler funktioniert. Grob gesagt führt tsc 2 Schritte aus:

  1. Typencheck

  2. Transpiling von TypeScript zu JavaScript

Es gibt hier einen krassen Gegensatz zu anderen gängigen Programmiersprachen wie Java, C# oder C++: Strong Typing existiert in TypeScript nur bis zur Compile Time. Zur Runtime wird ja JavaScript verwenden. Dabei versucht der Compiler, so viel JavaScript-Features wie möglich zu erhalten. Das hat zur Folge, dass const (welches ein JavaScript-Keyword ist) auch zur Runtime gilt (vorausgesetzt, die Target-Version von JavaScript supported const), aber as const und readonly als reine TypeScript-Keywords nur während der Kompilierzeit greifen.

Sehen wir uns folgenden TypeScript-Code an:

const colors = ['red', 'green', 'blue']
let colors2 = ['red', 'green', 'blue'] as const
const colors3 = ['red', 'green', 'blue'] as const

Dieser wird (mit Target: ES2021) in folgenden JavaScript-Code übersetzt :

const colors = ['red', 'green', 'blue']
let colors2 = ['red', 'green', 'blue']
const colors3 = ['red', 'green', 'blue']

Wir sehen, dass as const also reine Type-Checking-Funktionalität hat, und den produzierten Target-Code nicht beeinflusst. Dasselbe gilt auch für readonly.

Typisierung in TypeScript

Die Typisierung funktioniert in TypeScript teilweise ebenfalls signifikant anders als zB in Java oder C#. Wird kein expliziter Typ angegeben, so versucht der Kompiler selbstständig herauszufinden, welcher Typ am besten für eine Variable (oder Konstante) geeignet ist. Eine Variable, der ein Text zugewiesen wird, wird als String typisiert:

let str1 = 'Foo'  // Type: string

Eine Konstante hingegen wird als Typ mit nur einem Wert typisiert:

const str2 = 'Bar' // Type: "Bar"

Das macht Sinn, denn die Konstante kann schließlich nur den Wert "Bar" annehmen.

Welche Typen haben die drei Arrays?

Nun die wichtige Frage: Wie sieht es nun mit den Arrays aus?

const colors = ['red', 'green', 'blue']            // Type: string[]
let colors2 = ['red', 'green', 'blue'] as const    // Type: readonly["red", "green", "blue"]
const colors3 = ['red', 'green', 'blue'] as const  // Type: readonly["red", "green", "blue"]

Bei Arrays wirkt sich das const Keyword alleine nicht auf den Typ aus, sondern nur darauf, ob es sich um eine Variable oder eine Konstante handelt:

  • const/let bestimmt nur, ob wir color/color2 einen neuen Wert zuordnen können oder nicht (zB ein komplett neues Array)

  • as const bestimmt, ob das zugewiesene Array readonly (also unveränderbar) ist, oder nicht. Der generierte Typ entspricht genau dem konkreten Array mit den 3 Farb-Strings als Inhalt

Die Auswirkungen von "const" und "as const"

Das Wichtigste vorweg: Ein "const"-Array (oder Objekt) ist veränderbar! Um sie readonly zu machen, wie wir es von anderen Programmiersprachen gewöhnt sind, müssen wir as const verwenden.

Hier die Details:

const verhindert, dass der Konstante ein anderer Ausdruck zugewiesen wird. Der Inhalt selbst (der im Hauptspeicher an einer anderen Stelle wird, auf der die Konstante lediglich verweist) kann aber verändert werden, sofern es sich um ein Array oder ein Objekt handelt:

const colors = ['red', 'green', 'blue']

colors.push('yellow')          // Ok -> Unintuitiver weise können wir das Array verändern, obwohl es "const" ist
colors = ['magenta', 'fuxia']  // Error: Cannot assign to 'colors' because it is a constant.

as const definiert einen (readonly) Typ der genau dem Inhalt des Arrays entspricht:

let colors2 = ['red', 'green', 'blue'] as const

colors2.push('yellow')          // Error: Property 'push' does not exist on type 'readonly ["red", "green", "blue"]'.
colors2 = ['magenta', 'fuxia']  // Error: Type '"magenta"' is not assignable to type '"red"'.

Spannend ist die zweite Fehlermeldung hier: Sie indiziert einen Typ-Mismatch. Da colors2 keine Konstante sondern eine Variable ist, ist eine Zuweisung prinzipiell möglich. Allerdings nur, wenn der zugewiesene Typ auch entsprechend readonly["red", "green", "blue"] ist. colors3 hat genau diesen Typ, daher könnte man diese Zuweisung machen, aber umgekehrt nicht (denn colors3 ist ja eine Konstante):

let colors2 = ['red', 'green', 'blue'] as const
const colors3 = ['red', 'green', 'blue'] as const

colors2 = colors3  // Ok
colors3 = colors2  // Error: Cannot assign to 'colors3' because it is a constant.

Auch "const"-Objekte sind veränderbar

Was für Arrays gilt, gilt auch für Objekte. Das const-Keyword verhindert zwar, dass einer Konstante ein neuer Wert zugeordnet wird, aber der Inhalt der Konstante kann geändert werden:

const car = {
    color: 'green',
    seats: 5
}
car.seats = 4          // Unintuitiv: Hier können wir das Feld "seats" ändern, obwohl myCar const ist
console.log(car.seats) // 4

Um das Objekt wirklich konstant zu machen, müssen wir as const verwenden:

const car = {
    color: 'green',
    seats: 5
} as const
car.seats = 4 // Cannot assign to 'seats' because it is a read-only property.

Praxistipp: readonly

Bisher haben wir das readonly Keyword nicht verwendet. Dies kommt bei der Definition von Funktionen zur Anwendung, wo wir es in der Typspezifikation von Parametern verwenden können:

function foo(colors: readonly string[]) {
    colors.push('olive')  // Error: Property 'push' does not exist on type 'readonly string[]'.
}

Eine weitere Verwendung findet readonly in Klassen. Hier können wir readonly-Felder erzeugen, welche (im Gegensatz zu Konstanten) einmalig im Konstruktor gesetzt werden und ab dann nicht mehr verändert werden dürfen:

class Animal {
    readonly name: string

    constructor(name: string) {
        this.name = name
    }

    public setName(name: string) {
        this.name = name   // Error: Cannot assign to 'name' because it is a read-only property.
    }
}

Conclusio

Defensives Programmieren hilft, Fehler zu vermeiden bzw. frühzeitig zu erkennen. Dabei ist es wichtig, die Möglichkeiten hierzu genau zu kennen:

  • const verhindert zwar direkte Zuweisungen, aber der Inhalt der Konstante kann geändert werden, wenn dieser Inhalt über Veränderungsmöglichkeiten verfügt, was im Fall von Arrays und Objekten zutrifft

  • as const macht den Inhalt einer Variable konstant (readonly) und verhindert bei Arrays und Objekten, dass deren Inhalt verändert werden kann

  • readonly ist ein Keyword um Funktionsparameter und Felder in Klassen als nicht veränderbar zu markieren

Liebst du TypeScript auch? Dann melde dich doch bei uns :)

Mehr TypeScript-Stuff

Apropos defensives Programmieren: Auf unserem Youtube-Channel erfährst du zB, wie du Arrays in TypeScript type-safe filtern kannst:

Zurück zur Übersicht

Get inTouch

Diese Seite wird durch reCAPTCHA geschützt. Es gelten Googles Datenschutzbestimmungen und Nutzungsbedingungen.
Adresse:
Hainburger Straße 33, 1030 Wien