const Enumsconst-Enumsconst-Enumskeyof und EnumsDieses Kapitel beantwortet die folgenden beiden Fragen
Im nächsten Kapitel betrachten wir Alternativen zu Enums.
boolean ist ein Typ mit einer endlichen Anzahl von Werten: false und true. Mit Enums können wir ähnliche Typen selbst definieren.
Dies ist ein numerisches Enum
enum NoYes {
No = 0,
Yes = 1, // trailing comma
}
assert.equal(NoYes.No, 0);
assert.equal(NoYes.Yes, 1);Erläuterungen
No und Yes werden als Mitglieder des Enums NoYes bezeichnet.No und den Wert 0.Wir können Mitglieder wie Literale verwenden, z. B. true, 123 oder 'abc' – zum Beispiel
function toGerman(value: NoYes) {
switch (value) {
case NoYes.No:
return 'Nein';
case NoYes.Yes:
return 'Ja';
}
}
assert.equal(toGerman(NoYes.No), 'Nein');
assert.equal(toGerman(NoYes.Yes), 'Ja');Anstelle von Zahlen können wir auch Zeichenketten als Enum-Mitgliedswerte verwenden
enum NoYes {
No = 'No',
Yes = 'Yes',
}
assert.equal(NoYes.No, 'No');
assert.equal(NoYes.Yes, 'Yes');Die letzte Art von Enums wird als heterogen bezeichnet. Die Mitgliedswerte eines heterogenen Enums sind eine Mischung aus Zahlen und Zeichenketten
enum Enum {
One = 'One',
Two = 'Two',
Three = 3,
Four = 4,
}
assert.deepEqual(
[Enum.One, Enum.Two, Enum.Three, Enum.Four],
['One', 'Two', 3, 4]
);Heterogene Enums werden nicht oft verwendet, da sie wenige Anwendungsfälle haben.
Leider unterstützt TypeScript nur Zahlen und Zeichenketten als Enum-Mitgliedswerte. Andere Werte, wie Symbole, sind nicht erlaubt.
Wir können Initialisierer in zwei Fällen weglassen
Dies ist ein numerisches Enum ohne Initialisierer
enum NoYes {
No,
Yes,
}
assert.equal(NoYes.No, 0);
assert.equal(NoYes.Yes, 1);Dies ist ein heterogenes Enum, bei dem einige Initialisierer weggelassen wurden
enum Enum {
A,
B,
C = 'C',
D = 'D',
E = 8, // (A)
F,
}
assert.deepEqual(
[Enum.A, Enum.B, Enum.C, Enum.D, Enum.E, Enum.F],
[0, 1, 'C', 'D', 8, 9]
);Beachten Sie, dass wir den Initialisierer in Zeile A nicht weglassen können, da der Wert des vorhergehenden Mitglieds keine Zahl ist.
Es gibt mehrere Präzedenzfälle für die Benennung von Konstanten (in Enums oder anderswo)
Number.MAX_VALUEMath.SQRT2Symbol.asyncIteratorNoYes-Enum verwendet.Ähnlich wie bei JavaScript-Objekten können wir die Namen von Enum-Mitgliedern in Anführungszeichen setzen
enum HttpRequestField {
'Accept',
'Accept-Charset',
'Accept-Datetime',
'Accept-Encoding',
'Accept-Language',
}
assert.equal(HttpRequestField['Accept-Charset'], 1);Es gibt keine Möglichkeit, die Namen von Enum-Mitgliedern zu berechnen. Objekt-Literale unterstützen berechnete Schlüsseleigenschaften über eckige Klammern.
TypeScript unterscheidet drei Arten von Enum-Mitgliedern, je nachdem, wie sie initialisiert werden
Ein Literal-Enum-Mitglied
Ein konstantes Enum-Mitglied wird über einen Ausdruck initialisiert, dessen Ergebnis zur Kompilierzeit berechnet werden kann.
Ein berechnetes Enum-Mitglied wird über einen beliebigen Ausdruck initialisiert.
Bisher haben wir nur Literal-Mitglieder verwendet.
In der vorherigen Liste sind die früher erwähnten Mitglieder weniger flexibel, unterstützen aber mehr Funktionen. Lesen Sie weiter für weitere Informationen.
Ein Enum-Mitglied ist literal, wenn sein Wert angegeben ist
Wenn ein Enum nur Literal-Mitglieder hat, können wir diese Mitglieder als Typen verwenden (ähnlich wie z. B. Zahlen-Literale als Typen verwendet werden können)
enum NoYes {
No = 'No',
Yes = 'Yes',
}
function func(x: NoYes.No) { // (A)
return x;
}
func(NoYes.No); // OK
// @ts-expect-error: Argument of type '"No"' is not assignable to
// parameter of type 'NoYes.No'.
func('No');
// @ts-expect-error: Argument of type 'NoYes.Yes' is not assignable to
// parameter of type 'NoYes.No'.
func(NoYes.Yes);NoYes.No in Zeile A ist ein Enum-Mitgliedstyp.
Zusätzlich unterstützen Literal-Enums Erschöpfungsprüfungen (die wir später noch betrachten werden).
Ein Enum-Mitglied ist konstant, wenn sein Wert zur Kompilierzeit berechnet werden kann. Daher können wir seinen Wert entweder implizit angeben (d. h. wir lassen TypeScript ihn für uns angeben). Oder wir geben ihn explizit an und dürfen nur die folgende Syntax verwenden
+, -, ~+, -, *, /, %, <<, >>, >>>, &, |, ^Dies ist ein Beispiel für ein Enum, dessen Mitglieder alle konstant sind (wir werden später sehen, wie dieses Enum verwendet wird)
enum Perm {
UserRead = 1 << 8, // bit 8
UserWrite = 1 << 7,
UserExecute = 1 << 6,
GroupRead = 1 << 5,
GroupWrite = 1 << 4,
GroupExecute = 1 << 3,
AllRead = 1 << 2,
AllWrite = 1 << 1,
AllExecute = 1 << 0,
}Im Allgemeinen können konstante Mitglieder nicht als Typen verwendet werden. Erschöpfungsprüfungen werden jedoch weiterhin durchgeführt.
Die Werte von berechneten Enum-Mitgliedern können über beliebige Ausdrücke angegeben werden. Zum Beispiel
enum NoYesNum {
No = 123,
Yes = Math.random(), // OK
}Dies war ein numerisches Enum. Auf Zeichenketten basierende Enums und heterogene Enums sind begrenzter. Zum Beispiel können wir keine Methodenaufrufe verwenden, um Mitgliedswerte anzugeben
enum NoYesStr {
No = 'No',
// @ts-expect-error: Computed values are not permitted in
// an enum with string valued members.
Yes = ['Y', 'e', 's'].join(''),
}TypeScript führt keine Erschöpfungsprüfungen für berechnete Enum-Mitglieder durch.
Beim Protokollieren von Mitgliedern numerischer Enums sehen wir nur Zahlen
enum NoYes { No, Yes }
console.log(NoYes.No);
console.log(NoYes.Yes);
// Output:
// 0
// 1Wenn das Enum als Typ verwendet wird, sind die statisch erlaubten Werte nicht nur die der Enum-Mitglieder – jede beliebige Zahl wird akzeptiert
enum NoYes { No, Yes }
function func(noYes: NoYes) {}
func(33); // no error!Warum gibt es keine strengeren statischen Prüfungen? Daniel Rosenwasser erklärt
Das Verhalten wird durch bitweise Operationen motiviert. Es gibt Zeiten, in denen
SomeFlag.Foo | SomeFlag.Bardazu bestimmt ist, ein anderesSomeFlagzu ergeben. Stattdessen erhalten Sienumber, und Sie möchten nicht jedes Mal zurück zuSomeFlagcasten müssen.Ich glaube, wenn wir TypeScript noch einmal machen und immer noch Enums hätten, hätten wir ein separates Konstrukt für Bit-Flags gemacht.
Wie Enums für Bitmuster verwendet werden, wird bald detaillierter gezeigt.
Meine Empfehlung ist, auf Zeichenketten basierende Enums zu bevorzugen (der Kürze halber folgt dieses Kapitel nicht immer dieser Empfehlung)
enum NoYes { No='No', Yes='Yes' }Einerseits ist die Protokollausgabe für Menschen nützlicher
console.log(NoYes.No);
console.log(NoYes.Yes);
// Output:
// 'No'
// 'Yes'Andererseits erhalten wir eine strengere Typüberprüfung
function func(noYes: NoYes) {}
// @ts-expect-error: Argument of type '"abc"' is not assignable
// to parameter of type 'NoYes'.
func('abc');
// @ts-expect-error: Argument of type '"Yes"' is not assignable
// to parameter of type 'NoYes'.
func('Yes'); // (A)Nicht einmal Zeichenketten, die gleich den Werten der Mitglieder sind, sind erlaubt (Zeile A).
Im Node.js-Dateisystemmodul haben mehrere Funktionen den Parameter mode. Er gibt Dateiberechtigungen über eine numerische Kodierung an, die ein Überbleibsel von Unix ist
Das bedeutet, dass Berechtigungen durch 9 Bits dargestellt werden können (3 Kategorien mit jeweils 3 Berechtigungen)
| Benutzer | Gruppe | Alle | |
|---|---|---|---|
| Berechtigungen | r, w, x | r, w, x | r, w, x |
| Bit | 8, 7, 6 | 5, 4, 3 | 2, 1, 0 |
Node.js macht das nicht, aber wir könnten ein Enum verwenden, um mit diesen Flags zu arbeiten
enum Perm {
UserRead = 1 << 8, // bit 8
UserWrite = 1 << 7,
UserExecute = 1 << 6,
GroupRead = 1 << 5,
GroupWrite = 1 << 4,
GroupExecute = 1 << 3,
AllRead = 1 << 2,
AllWrite = 1 << 1,
AllExecute = 1 << 0,
}Bitmuster werden über Bitweises ODER kombiniert
// User can change, read and execute.
// Everyone else can only read and execute.
assert.equal(
Perm.UserRead | Perm.UserWrite | Perm.UserExecute |
Perm.GroupRead | Perm.GroupExecute |
Perm.AllRead | Perm.AllExecute,
0o755);
// User can read and write.
// Group members can read.
// Everyone can’t access at all.
assert.equal(
Perm.UserRead | Perm.UserWrite | Perm.GroupRead,
0o640);Die Grundidee hinter Bitmustern ist, dass es eine Menge von Flags gibt und jede Teilmenge dieser Flags ausgewählt werden kann.
Daher ist die Verwendung echter Mengen zur Auswahl von Teilmengen ein direkterer Weg, um die gleiche Aufgabe auszuführen
enum Perm {
UserRead = 'UserRead',
UserWrite = 'UserWrite',
UserExecute = 'UserExecute',
GroupRead = 'GroupRead',
GroupWrite = 'GroupWrite',
GroupExecute = 'GroupExecute',
AllRead = 'AllRead',
AllWrite = 'AllWrite',
AllExecute = 'AllExecute',
}
function writeFileSync(
thePath: string, permissions: Set<Perm>, content: string) {
// ···
}
writeFileSync(
'/tmp/hello.txt',
new Set([Perm.UserRead, Perm.UserWrite, Perm.GroupRead]),
'Hello!');Manchmal haben wir Sätze von Konstanten, die zusammengehören
const off = Symbol('off');
const info = Symbol('info');
const warn = Symbol('warn');
const error = Symbol('error');Dies ist ein guter Anwendungsfall für ein Enum
enum LogLevel {
off = 'off',
info = 'info',
warn = 'warn',
error = 'error',
}Ein Vorteil des Enums ist, dass die Konstantenamen gruppiert und in den Namespace LogLevel verschachtelt sind.
Ein weiterer Vorteil ist, dass wir automatisch den Typ LogLevel dafür erhalten. Wenn wir einen solchen Typ für die Konstanten wollen, brauchen wir mehr Aufwand
type LogLevel =
| typeof off
| typeof info
| typeof warn
| typeof error
;Weitere Informationen zu diesem Ansatz finden Sie in §13.1.3 „Unionen von Symbol-Singleton-Typen“.
Wenn Booleans verwendet werden, um Alternativen darzustellen, sind Enums normalerweise aussagekräftiger.
Um beispielsweise darzustellen, ob eine Liste geordnet ist oder nicht, können wir einen Boolean verwenden
class List1 {
isOrdered: boolean;
// ···
}Ein Enum ist jedoch aussagekräftiger und hat den zusätzlichen Vorteil, dass wir später weitere Alternativen hinzufügen können, wenn wir sie brauchen.
enum ListKind { ordered, unordered }
class List2 {
listKind: ListKind;
// ···
}Ähnlich können wir angeben, wie Fehler behandelt werden sollen, über einen Boolean-Wert
function convertToHtml1(markdown: string, throwOnError: boolean) {
// ···
}Oder wir können dies über einen Enum-Wert tun
enum ErrorHandling {
throwOnError = 'throwOnError',
showErrorsInContent = 'showErrorsInContent',
}
function convertToHtml2(markdown: string, errorHandling: ErrorHandling) {
// ···
}Betrachten Sie die folgende Funktion, die reguläre Ausdrücke erstellt.
const GLOBAL = 'g';
const NOT_GLOBAL = '';
type Globalness = typeof GLOBAL | typeof NOT_GLOBAL;
function createRegExp(source: string,
globalness: Globalness = NOT_GLOBAL) {
return new RegExp(source, 'u' + globalness);
}
assert.deepEqual(
createRegExp('abc', GLOBAL),
/abc/ug);
assert.deepEqual(
createRegExp('abc', 'g'), // OK
/abc/ug);Anstelle der Zeichenketten-Konstanten können wir ein Enum verwenden
enum Globalness {
Global = 'g',
notGlobal = '',
}
function createRegExp(source: string, globalness = Globalness.notGlobal) {
return new RegExp(source, 'u' + globalness);
}
assert.deepEqual(
createRegExp('abc', Globalness.Global),
/abc/ug);
assert.deepEqual(
// @ts-expect-error: Argument of type '"g"' is not assignable to parameter of type 'Globalness | undefined'. (2345)
createRegExp('abc', 'g'), // error
/abc/ug);Was sind die Vorteile dieses Ansatzes?
Globalness akzeptiert nur Mitgliedsnamen, keine Zeichenketten.TypeScript kompiliert Enums in JavaScript-Objekte. Als Beispiel nehmen wir das folgende Enum
enum NoYes {
No,
Yes,
}TypeScript kompiliert dieses Enum zu
var NoYes;
(function (NoYes) {
NoYes[NoYes["No"] = 0] = "No";
NoYes[NoYes["Yes"] = 1] = "Yes";
})(NoYes || (NoYes = {}));In diesem Code werden die folgenden Zuweisungen vorgenommen
NoYes["No"] = 0;
NoYes["Yes"] = 1;
NoYes[0] = "No";
NoYes[1] = "Yes";Es gibt zwei Gruppen von Zuweisungen
Gegeben sei ein numerisches Enum
enum NoYes {
No,
Yes,
}Die normale Abbildung ist von Mitgliedsnamen zu Mitgliedswerten
// Static (= fixed) lookup:
assert.equal(NoYes.Yes, 1);
// Dynamic lookup:
assert.equal(NoYes['Yes'], 1);Numerische Enums unterstützen auch eine umgekehrte Abbildung von Mitgliedswerten zu Mitgliedsnamen
assert.equal(NoYes[1], 'Yes');Ein Anwendungsfall für umgekehrte Abbildungen ist das Drucken des Namens eines Enum-Mitglieds
function getQualifiedName(value: NoYes) {
return 'NoYes.' + NoYes[value];
}
assert.equal(
getQualifiedName(NoYes.Yes), 'NoYes.Yes');Auf Zeichenketten basierende Enums haben eine einfachere Darstellung zur Laufzeit.
Betrachten Sie das folgende Enum.
enum NoYes {
No = 'NO!',
Yes = 'YES!',
}Es wird zu diesem JavaScript-Code kompiliert
var NoYes;
(function (NoYes) {
NoYes["No"] = "NO!";
NoYes["Yes"] = "YES!";
})(NoYes || (NoYes = {}));TypeScript unterstützt keine umgekehrten Abbildungen für auf Zeichenketten basierende Enums.
const EnumsWenn ein Enum mit dem Schlüsselwort const präfigiert ist, hat es keine Darstellung zur Laufzeit. Stattdessen werden die Werte seiner Mitglieder direkt verwendet.
const-EnumsUm diesen Effekt zu beobachten, betrachten wir zunächst das folgende Nicht-const-Enum
enum NoYes {
No = 'No',
Yes = 'Yes',
}
function toGerman(value: NoYes) {
switch (value) {
case NoYes.No:
return 'Nein';
case NoYes.Yes:
return 'Ja';
}
}TypeScript kompiliert diesen Code zu
"use strict";
var NoYes;
(function (NoYes) {
NoYes["No"] = "No";
NoYes["Yes"] = "Yes";
})(NoYes || (NoYes = {}));
function toGerman(value) {
switch (value) {
case NoYes.No:
return 'Nein';
case NoYes.Yes:
return 'Ja';
}
}const-EnumsDies ist derselbe Code wie zuvor, aber jetzt ist das Enum const
const enum NoYes {
No,
Yes,
}
function toGerman(value: NoYes) {
switch (value) {
case NoYes.No:
return 'Nein';
case NoYes.Yes:
return 'Ja';
}
}Jetzt verschwindet die Darstellung des Enums als Konstrukt und nur die Werte seiner Mitglieder bleiben erhalten
function toGerman(value) {
switch (value) {
case "No" /* No */:
return 'Nein';
case "Yes" /* Yes */:
return 'Ja';
}
}TypeScript behandelt (Nicht-const-)Enums so, als wären sie Objekte
enum NoYes {
No = 'No',
Yes = 'Yes',
}
function func(obj: { No: string }) {
return obj.No;
}
assert.equal(
func(NoYes), // allowed statically!
'No');Wenn wir einen Enum-Mitgliedswert akzeptieren, möchten wir oft sicherstellen, dass
Lesen Sie weiter für weitere Informationen. Wir werden mit dem folgenden Enum arbeiten
enum NoYes {
No = 'No',
Yes = 'Yes',
}Im folgenden Code treffen wir zwei Maßnahmen gegen ungültige Werte
function toGerman1(value: NoYes) {
switch (value) {
case NoYes.No:
return 'Nein';
case NoYes.Yes:
return 'Ja';
default:
throw new TypeError('Unsupported value: ' + JSON.stringify(value));
}
}
assert.throws(
// @ts-expect-error: Argument of type '"Maybe"' is not assignable to
// parameter of type 'NoYes'.
() => toGerman1('Maybe'),
/^TypeError: Unsupported value: "Maybe"$/);Die Maßnahmen sind
NoYes, dass ungültige Werte an den Parameter value übergeben werden.default-Fall verwendet, um eine Ausnahme auszulösen, wenn ein unerwarteter Wert vorhanden ist.Wir können eine weitere Maßnahme ergreifen. Der folgende Code führt eine Erschöpfungsprüfung durch: TypeScript warnt uns, wenn wir vergessen, alle Enum-Mitglieder zu berücksichtigen.
class UnsupportedValueError extends Error {
constructor(value: never) {
super('Unsupported value: ' + value);
}
}
function toGerman2(value: NoYes) {
switch (value) {
case NoYes.No:
return 'Nein';
case NoYes.Yes:
return 'Ja';
default:
throw new UnsupportedValueError(value);
}
}Wie funktioniert die Erschöpfungsprüfung? Für jeden Fall leitet TypeScript den Typ von value ab
function toGerman2b(value: NoYes) {
switch (value) {
case NoYes.No:
// %inferred-type: NoYes.No
value;
return 'Nein';
case NoYes.Yes:
// %inferred-type: NoYes.Yes
value;
return 'Ja';
default:
// %inferred-type: never
value;
throw new UnsupportedValueError(value);
}
}Im default-Fall leitet TypeScript den Typ never für value ab, da wir dort nie ankommen. Wenn wir jedoch ein Mitglied .Maybe zu NoYes hinzufügen, ist der abgeleitete Typ von value NoYes.Maybe. Und dieser Typ ist statisch inkompatibel mit dem Typ never des Parameters von new UnsupportedValueError(). Deshalb erhalten wir die folgende Fehlermeldung zur Kompilierzeit
Argument of type 'NoYes.Maybe' is not assignable to parameter of type 'never'.
Praktischerweise funktioniert diese Art von Erschöpfungsprüfung auch mit if-Anweisungen
function toGerman3(value: NoYes) {
if (value === NoYes.No) {
return 'Nein';
} else if (value === NoYes.Yes) {
return 'Ja';
} else {
throw new UnsupportedValueError(value);
}
}Alternativ erhalten wir auch eine Erschöpfungsprüfung, wenn wir einen Rückgabetyp angeben
function toGerman4(value: NoYes): string {
switch (value) {
case NoYes.No:
const x: NoYes.No = value;
return 'Nein';
case NoYes.Yes:
const y: NoYes.Yes = value;
return 'Ja';
}
}Wenn wir ein Mitglied zu NoYes hinzufügen, beschwert sich TypeScript, dass toGerman4() möglicherweise undefined zurückgibt.
Nachteile dieses Ansatzes
if-Anweisungen (weitere Informationen).keyof und EnumsWir können den keyof-Typoperator verwenden, um den Typ zu erstellen, dessen Elemente die Schlüssel der Enum-Mitglieder sind. Wenn wir dies tun, müssen wir keyof mit typeof kombinieren
enum HttpRequestKeyEnum {
'Accept',
'Accept-Charset',
'Accept-Datetime',
'Accept-Encoding',
'Accept-Language',
}
// %inferred-type: "Accept" | "Accept-Charset" | "Accept-Datetime" |
// "Accept-Encoding" | "Accept-Language"
type HttpRequestKey = keyof typeof HttpRequestKeyEnum;
function getRequestHeaderValue(request: Request, key: HttpRequestKey) {
// ···
}keyof ohne typeofWenn wir keyof ohne typeof verwenden, erhalten wir einen anderen, weniger nützlichen Typ
// %inferred-type: "toString" | "toFixed" | "toExponential" |
// "toPrecision" | "valueOf" | "toLocaleString"
type Keys = keyof HttpRequestKeyEnum;keyof HttpRequestKeyEnum ist dasselbe wie keyof number.
@spira_mirabilis für sein Feedback zu diesem Kapitel.