12. Okt 2020
Lesedauer 7 Min.
Alles über Enums
TypeScript: Aufzählungen
Im Gegensatz zu JavaScript kennt TypeScript den Datentyp des Enum. Wie funktionieren Enums in TypeScript und was gilt es bei deren Einsatz zu beachten?

JavaScript kennt nur sieben Datentypen, nämlich string, number, boolean, undefined, function, object und symbol. Nimmt man es ganz genau, gibt es mit null auch noch einen achten Datentyp, der praktisch aber keine Rolle spielt und nicht ansprechbar ist. Einer der am schmerzlichsten vermissten Datentypen in JavaScript ist die Enumeration, abgekürzt Enum, mit der man Aufzählungen definieren kann. Das kann praktisch sein, um eine Reihe von möglichen Werten zu definieren, die einer Variablen zugewiesen werden können. Je nachdem, wie man diese definiert, lassen sich sogar mehrere Werte zugleich zuweisen.In JavaScript haben sich daher im Lauf der Zeit verschiedene Ansätze entwickelt, wie man diesen fehlenden Datentyp nachrüsten kann. Doch egal wie man es anstellt, am Ende läuft es stets auf zwei grundlegende Konzepte hinaus. Die erste Möglichkeit ist, Bezeichner für Zahlen zu definieren. Auf diesem Weg kann man beispielsweise ein Objekt anlegen, das alle erlaubten Werte als Keys enthält, und diesem Objekt dann verschiedene Zahlen als Werte zuweisen:
const color = {
gold: 1,
rose: 2,
gray: 3
};
Anschließend ist es möglich, diese Werte zu verwenden. Das Praktische daran ist, dass sich die Werte nicht nur zuweisen und auslesen lassen, sondern dass sie auch verglichen werden können – und über sprechende Namen verfügen:
const phone = {
name: 'iPhone',
color: color.rose
};
Der größte Haken in JavaScript ist dabei allerdings, dass man sich allzu leicht bei dem Namen eines Keys verschreiben kann, was dann nicht zu einer Fehlermeldung führt, sondern dazu, dass die angesprochene Property schlicht nicht gefunden wird und man auf diesem Weg undefined als Wert erhält.Das fällt, wenn überhaupt, erst zur Laufzeit auf, und das unter Umständen auch nur sehr spät und in einem ganz anderen Kontext.Daher ist die zweite Option in JavaScript, direkt mit Strings zu arbeiten. Der Gedanke dahinter: Wenn man ohnehin keine Typsicherheit erhält, kann man sich den ganzen Aufwand auch sparen:
const phone = {
name: 'iPhone',
color: 'rose'
};
Allerdings verliert man auf diesem Weg den Überblick, welche Werte überhaupt zulässig sind. Daher bietet sich manchmal eine Mischung aus beiden Welten an, die das Problem aber auch nur wieder verschiebt und es nicht löst:
const color = {
gold: 'gold',
rose: 'rose',
gray: 'gray'
};
Besondere Enums
Gelegentlich soll es möglich sein, nicht nur einen Wert eines Enum zuzuweisen, sondern mehrere gleichzeitig. Dann bietet sich in JavaScript der gleiche Trick an, der auch schon bei C# mit dem Flags-Attribut funktioniert, nämlich, die verwendeten Zahlen als Zweierpotenzen zu definieren. Diese lassen sich dann elegant mithilfe der binären Operatoren verknüpfen. Außerdem empfiehlt es sich, in diesem Fall einen Wert für none zu definieren, der üblicherweise 0 ist:
const fileAccess = {
none: 0,
execute: 1,
write: 2,
read: 4
};
Nun könnte man beispielsweise eine Datei als les- und schreibbar markieren, aber nicht als ausführbar. Dazu müssen die Werte 2 und 4 kombiniert werden, was mithilfe des binären Oder-Operators gelingt:
const file = {
name: 'data.txt',
mode: fileAccess.write | fileAccess.read
};
console.log(file);
// => { name: 'data.txt', mode: 6 }
Das wäre mit dem zweiten beschriebenen Ansatz, direkt mit Zeichenketten zu arbeiten, nicht möglich, da diese sich nicht sinnvoll auf Binärebene verknüpfen lassen. Trotzdem wirkt das Ganze nach wie vor eher wie ein Workaround als wie eine gute Lösung.
Enums in TypeScript
Glücklicherweise schafft TypeScript hier Abhilfe, denn die Sprache enthält einen Typ für Enums, die sich mit dem Schlüsselwort enum definieren lassen. Dazu genügt es prinzipiell, die gewünschten Werte innerhalb des Enum aufzuzählen:
enum Color {
Gold,
Rose,
Gray
}
Unter der Haube werden auch hier die einzelnen Werte durchnummeriert, allerdings beginnt TypeScript mit der Zählung bei 0. Wer das nicht möchte, kann den gewünschten Startwert auch manuell angeben:
enum Color {
Gold = 1,
Rose,
Gray
}
Gelegentlich kann es erwünscht sein, ein Enum sinnvoll serialisieren zu können. Das ist mit einem zahlenbasierten Enum zwar grundsätzlich möglich, aber die Zahlen haben keinerlei fachliche Bedeutung. Daher besteht in TypeScript auch die Option, ein Enum auf der Basis von Zeichenketten zu definieren:
enum Color {
Gold = 'Gold',
Rose = 'Rose',
Gray = 'Gray'
}
Aus technischer Sicht ist es sogar möglich, beide Welten zu mischen, das heißt, es ist durchaus zulässig, dass einzelne Werte als Zahlen, andere hingegen als Zeichenketten definiert werden. Dies erweist sich in der Praxis nur höchst selten als sinnvoll, aber trotzdem ist es interessant zu wissen.Das zuvor genannte Beispiel der Aufzählung fileAccess, bei der sich mehrere Werte kombinieren ließen, ist mit TypeScript auf die gleiche Art umsetzbar wie in JavaScript. Es genügt, die Werte mit Zweierpotenzen zu versehen. Allerdings gilt auch hier, dass es ratsam ist, einen Wert für None mit 0 zu definieren. Außerdem ist es möglich, kombinierte Werte bereits im Enum zu definieren:
enum FileAccess {
None = 0,
Execute = 1,
Write = 2,
Read = 4,
ReadWrite = Write | Read
}
Die technische Umsetzung
Spannend ist, wie TypeScript das Sprachfeature umsetzt, denn letztlich muss der in TypeScript geschriebene Code wieder nach JavaScript übersetzt werden. Dazu empfiehlt sich ein Blick in den übersetzten Code. Übergibt man das zuvor bereits verwendete Fragment
enum Color {
Gold = 1,
Rose,
Gray
}
an den TypeScript-Compiler, erzeugt dieser den folgenden JavaScript-Code:
"use strict";
var Color;
(function (Color) {
Color[Color["Gold"] = 1] = "Gold";
Color[Color["Rose"] = 2] = "Rose";
Color[Color["Gray"] = 3] = "Gray";
})(Color || (Color = {}));
Wie man sieht, wird zunächst ein Objekt namens Color definiert; anschließend erfolgt die Übergabe an eine Funktion, die sofort ausgeführt wird. Dabei wird das Objekt, falls es noch nicht initialisiert wurde, als leeres Objekt angelegt. Innerhalb der Funktion erkennt man die Zuweisungen der einzelnen Werte, allerdings wirken die einzelnen Zeilen seltsam:
Color[Color["Gold"] = 1] = "Gold";
Hier hilft es, diese von innen nach außen zu analysieren. Zunächst wird an dem Objekt Color eine Eigenschaft namens Gold angelegt und deren Wert auf 1 gesetzt:
Color["Gold"] = 1
Wichtig zu wissen ist, dass eine Zuweisung in JavaScript stets den zugewiesenen Wert als Rückgabewert zurückgibt. Das heißt, die Zuweisung gibt hier wiederum den Wert 1 zurück. Aus diesem Grund lassen sich Zuweisungen in JavaScript verketten. Das ist zwar kein guter Stil, aber technisch gesehen durchaus valide:
const a = b = c = 1;
console.log(a); // => 1
console.log(b); // => 1
console.log(c); // => 1
Genau das passiert nun auch in der gesamten Zeile: Die innere Zuweisung gibt den Wert 1 zurück, weshalb an dem Objekt Color eine weitere Eigenschaft angelegt wird, dieses Mal mit dem Namen 1, und als Wert wird an der Stelle nun Gold gesetzt.Sinn dessen ist, dass man nicht nur anhand des Namens auf die Zahl schließen kann, sondern dass auch der Rückweg möglich ist, was zum Beispiel für die Anzeige von Enum-Werten durchaus hilfreich sein kann:
const gold: Color = Color.Gold;
console.log(Color[gold]);
// => 'Gold'
Das zeigt, dass TypeScript unter der Haube letztlich nichts anderes macht als JavaScript auch, den Code aber um gängige Komfortfunktionen für Enums ergänzt.Interessant ist auch, dass im kompilierten Code stets mit einer Indirektion gearbeitet wird, was zwar funktioniert, aus Performancegründen aber unter Umständen unerwünscht sein kann. Um dies zu optimieren, kann man ein Enum in TypeScript auch mit dem Schlüsselwort const definieren:
const enum Color {
Gold = 1,
Rose,
Gray
}
Übergibt man dieses Fragment an den TypeScript-Compiler, erhält man folgendes Kompilat:
"use strict";
Das ganze Enum ist nun verschwunden! Was hier geschieht, wird klarer, wenn man auch noch die Zuweisung an den Compiler übergibt. Aus
const enum Color {
Gold = 1,
Rose,
Gray
}
const gold: Color = Color.Gold;
wird dann nämlich
"use strict";
const gold = 1 /* Gold */;
Wie man sieht, wird die Indirektion nun aufgelöst, indem die Enums durch die verwendeten Zahlen ersetzt werden. Damit verliert man allerdings die Möglichkeit, wieder auf den Namen des Wertes schließen zu können, was zuvor möglich war. Der Versuch, das zu tun, scheitert mit einer entsprechenden Fehlermeldung des Compilers.Insgesamt erinnert das Vorgehen von TypeScript im Hinblick auf Enums mit und ohne const sehr an die feinen Unterschiede von C# hinsichtlich const und readonly [1].
Alternativen
Ein Enum ist in TypeScript nicht die einzige Möglichkeit, einen solchen Datentyp zur Auswahl bestimmter Werte zu schreiben. Alternativ kann man auch ein sogenanntes „Discriminated Union“ mit dem type-Schlüsselwort erzeugen und dort entsprechende Zeichenketten definieren:
type Color = 'Gold' | 'Rose' | 'Gray';
const gold: Color = 'Gold';
Das ist, wenn man so will, eine leichtgewichtigere Vorgehensweise, da man nicht ständig den Datentyp Color angeben muss, wenn man einen Wert zuweisen will. Stattdessen wird schlichtweg mit Zeichenketten gearbeitet, was viel näher an dem ist, wie JavaScript an sich funktioniert. Auch das Zuweisen mehrerer Werte funktioniert ohne Weiteres, indem man ein Array der entsprechenden Werte zulässt.In gewissem Sinne stellt dieser Ansatz also die Verwendung klassischer Zeichenketten dar, die aber auf einige wenige gültige Werte eingeschränkt werden. Das Schöne daran ist, dass man kein weiteres Konstrukt wie enum benötigt, sondern mit type auskommt. Der Typsicherheit tut das keinen Abbruch. Prinzipiell funktioniert das natürlich auch mit Zahlen, aber dann kann man die Zahlen nicht mehr ohne manuellen Aufwand in die ursprünglichen Bezeichnungen zurückverwandeln, wie das bei einem Enum der Fall ist.Es gilt also, wie so oft, Pro und Kontra der gezeigten Vorgehensweisen abzuwägen. Für welche man sich entscheidet, ist letztlich egal – am wichtigsten ist dabei sicherlich, eine einheitliche Linie für das gesamte Projekt zu finden, damit in einem Entwicklerteam Code nicht mal so, mal so geschrieben wird.
Fussnoten
- What is the difference between const and readonly in C#?, https://stackoverflow.com/a/56024/1333873