Enum w TypeScript to nazwany zbiór stałych. enum Direction { Up, Down, Left, Right } tworzy jednocześnie typ Direction i obiekt istniejący w czasie działania programu, do którego członów odwołujesz się przez Direction.Up. Człony są numerowane od 0, chyba że nadasz im wartości, a enumy tekstowe dają każdemu członowi czytelny string.
Enumy to jedna z nielicznych funkcji TypeScriptu, które nie są wyłącznie typami: po kompilacji enum staje się prawdziwym obiektem JavaScriptu.
Enumy numeryczne
Bez inicjalizatorów człony dostają 0, 1, 2 i tak dalej. Jeśli nadasz liczbę pierwszemu członowi, kolejne liczą dalej od niej. Możesz też ustawić każdą wartość jawnie, co jest bezpiecznym wyborem, gdy liczby trafiają do bazy danych albo są przesyłane przez sieć.
Automatyczne numerowanie wystarcza dla wartości, które nigdy nie opuszczają programu. Jeśli kolejność członów może się zmienić, a liczby są gdziekolwiek zapisywane, wstawienie członu w środek po cichu przenumeruje wszystko, co jest za nim.
Do czego kompiluje się enum
Typy są wymazywane, ale enum nie. Tak wygląda JavaScript, który TypeScript generuje dla enuma numerycznego i tekstowego:
enum Direction { Up, Down, Left, Right }
enum Status { Active = "ACTIVE", Inactive = "INACTIVE" }
var Direction;
(function (Direction) {
Direction[Direction["Up"] = 0] = "Up";
Direction[Direction["Down"] = 1] = "Down";
Direction[Direction["Left"] = 2] = "Left";
Direction[Direction["Right"] = 3] = "Right";
})(Direction || (Direction = {}));
var Status;
(function (Status) {
Status["Active"] = "ACTIVE";
Status["Inactive"] = "INACTIVE";
})(Status || (Status = {}));
Direction["Up"] = 0 zwraca 0, więc w tej samej instrukcji ustawiane jest też Direction[0] = "Up". Enum numeryczny działa więc w obie strony: z nazwy na liczbę i z liczby z powrotem na nazwę. To jest mapowanie odwrotne (reverse mapping). Enumy tekstowe mapują tylko nazwy na wartości.
Wypisany obiekt Direction ma osiem kluczy: cztery nazwy i cztery liczby. Ma to znaczenie, gdy tylko zaczniesz po nim iterować.
Enumy tekstowe
Każdy człon enuma tekstowego potrzebuje jawnej wartości tekstowej. Wartości pojawiają się bez zmian w logach, JSON-ie i bazach danych, dlatego enumy tekstowe łatwiej debugować niż liczby.
Enum tekstowy jest pod jednym względem nominalny, co często zaskakuje: zwykłego stringa nie można do niego przypisać, nawet jeśli tekst jest taki sam jak wartość któregoś członu.
index.ts(7,5): error TS2820: Type '"ACTIVE"' is not assignable to type 'Status'. Did you mean 'Status.Inactive'?
(Podpowiedź w komunikacie to zgadywanie kompilatora i tutaj jest błędna; poprawka to Status.Active.) W drugą stronę wartość typu Status można użyć wszędzie tam, gdzie oczekiwany jest string. Gdy wartości przychodzą jako stringi, z JSON-a albo z formularza, zamieniaj je z użyciem sprawdzenia takiego jak w sekcji o sprawdzaniu wartości poniżej.
Enum jako typ
Nazwa enuma jest typem, którego wartościami są jego człony. W połączeniu ze switch TypeScript sprawdza, czy obsłużono każdy człon, gdy funkcja musi zwrócić wartość:
Jeśli do Shape dojdzie nowy człon bez nowego case, sides przestanie się kompilować z błędem TS2366, Function lacks ending return statement and return type does not include 'undefined'. Strona o switch pokazuje ostrzejsze sprawdzenie wyczerpujące oparte na never.
Ostatnie linie pokazują prawdziwą słabość enumów numerycznych. Literał liczbowy, który nie pasuje do żadnego członu, const level: Level = 99, to błąd kompilacji (TS2322), ale każda wartość typu number zostanie przyjęta, więc 57 przechodzi. Enumy tekstowe nie mają tej luki.
Iterowanie po enumie
W czasie działania enum jest obiektem, więc działają Object.keys, Object.values i Object.entries. Dla enuma tekstowego zwracają dokładnie jego człony. Dla enuma numerycznego zwracają też wpisy mapowania odwrotnego, które trzeba odfiltrować:
Aby zmienna miała typ „jedna z nazw członów enuma”, użyj keyof typeof Direction, czyli unii "Up" | "Down" | "Left" | "Right". Wtedy Direction[name] odczytuje wartość z pełnym bezpieczeństwem typów.
Enum tekstowy nie ma mapowania odwrotnego, więc żeby uzyskać nazwę członu z jego wartości, przeszukaj wpisy: Object.entries(Status).find(([, v]) => v === "ACTIVE")?.[0] daje "Active" albo undefined, gdy żaden człon nie ma takiej wartości.
Sprawdzanie, czy wartość należy do enuma
Dane spoza programu to zwykły string albo number. Strażnik typu sprawdza je względem wartości enuma i zawęża do typu enuma:
Unikaj raw as Status dla niezaufanych danych: asercja się kompiluje, ale w czasie działania nic nie jest sprawdzane, więc "DELETED" wędrowałoby przez program z typem poprawnego Status.
const enum
const enum prosi kompilator, żeby usunął enum i w każdym miejscu użycia wpisał wartość członu. W czasie działania nie ma żadnego obiektu, więc nie da się po nim iterować ani korzystać z mapowania odwrotnego.
const enum oszczędza kilka bajtów i jedno odczytanie właściwości, ale wymaga, żeby kompilator widział deklarację enuma przy kompilacji każdego pliku, który go używa. Narzędzia transpilujące po jednym pliku, takie jak Babel i swc, nie widzą const enuma zadeklarowanego w innym pliku; usuwanie typów w Node odrzuca const enumy tak samo jak każdy inny enum; a przy isolatedModules albo verbatimModuleSyntax TypeScript zgłasza błąd TS2748, gdy używasz const enuma z pliku deklaracji. Większość kodu aplikacji nie potrzebuje const enumów.
Enum a typ unii i obiekt as const
Są trzy popularne sposoby na zdefiniowanie stałego zbioru wartości:
enum | Unia literałów | Obiekt as const | |
|---|---|---|---|
| Istnieje w czasie działania | tak, jako obiekt | nie | tak, jako zwykły obiekt |
| Iterowanie po wartościach | Object.values (numeryczny: z filtrem) | nie, nie ma po czym | Object.values |
Przyjmuje zwykłe "red" | nie (enumy tekstowe) | tak | tak |
Dostęp po nazwie X.Red | tak | nie | tak |
| Mapowanie odwrotne | tylko enumy numeryczne | nie | nie |
| Działa z usuwaniem typów w Node | nie | tak | tak |
Dozwolony przy erasableSyntaxOnly | nie | tak | tak |
| Dodatkowa składnia do nauki | reguły enumów, const enumy | żadna | wzorzec z typeof |
Wiele zespołów domyślnie wybiera dziś unię literałów tekstowych, a na obiekt as const przechodzi, gdy potrzebuje wartości w czasie działania (żeby po nich iterować albo zbudować listę rozwijaną). Powody: unie są czystymi typami i znikają z wyniku kompilacji; przyjmują zwykłe stringi, które dostarczają JSON i API; a enumy to jedyny element codziennego TypeScriptu, który nie jest „JavaScriptem plus wymazywalnymi typami”.
Ten ostatni punkt ma dziś praktyczne znaczenie. Node uruchamia pliki .ts bezpośrednio, usuwając typy, a enuma nie da się po prostu usunąć:
node status.ts
SyntaxError [ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX]: TypeScript enum is not supported in strip-only mode
Flaga Node --experimental-transform-types pozwala uruchomić enumy, a opcja kompilatora erasableSyntaxOnly zgłasza każdy enum jako błąd TS1294, This syntax is not allowed when 'erasableSyntaxOnly' is enabled., więc projekt może zakazać ich z góry. Jak działa usuwanie typów, wyjaśnia strona o uruchamianiu TypeScriptu. Nic z tego nie oznacza, że enumy są złe: kod kompilowany przez tsc albo bundler uruchamia je bez problemu, a projekt, który już używa enumów, niewiele zyska na ich przepisaniu.
Najczęściej zadawane pytania
Czym jest enum w TypeScript?
To nazwany zbiór stałych, który jest jednocześnie typem i obiektem w czasie działania programu: enum Direction { Up, Down } pozwala pisać Direction.Up i używać Direction jako typu parametru. W odróżnieniu od większości funkcji TypeScriptu enum nie jest wymazywany: kompiluje się do obiektu JavaScriptu, który istnieje w czasie działania.
Jak iterować po enumie w TypeScript?
W enumie tekstowym Object.values(MyEnum) zwraca wartości, a Object.keys(MyEnum) nazwy. Enum numeryczny zawiera też wpisy mapowania odwrotnego ("0": "Up"), więc trzeba je odfiltrować: Object.keys(Direction).filter((k) => isNaN(Number(k))) zwraca same nazwy. Po const enum nie da się iterować, bo nie istnieje on w czasie działania.
Jak zamienić string na wartość enuma w TypeScript?
Sprawdź string względem wartości enuma w strażniku typu: function isStatus(s: string): s is Status { return (Object.values(Status) as string[]).includes(s); }. Po tym sprawdzeniu s ma typ Status. Samo s as Status się kompiluje, ale w czasie działania niczego nie sprawdza.
Enum czy typ unii w TypeScript?
Wiele zespołów woli unię literałów tekstowych (type Status = "active" | "inactive") albo obiekt as const, gdy wartości są potrzebne także w czasie działania. Unie są całkowicie wymazywane, działają z wbudowanym w Node usuwaniem typów (type stripping) i opcją erasableSyntaxOnly oraz przyjmują zwykłe stringi, takie jak "active". Enumy też są w porządku, zwłaszcza w kodzie, który już ich używa.
Czym różni się enum od const enum?
Zwykły enum kompiluje się do obiektu, po którym można iterować i w którym można wyszukiwać wartości w czasie działania. const enum znika podczas kompilacji, a każde jego użycie zostaje zastąpione wartością (Size.Large staje się 2), więc nic nie kosztuje w czasie działania, ale nie da się po nim iterować, a narzędzia kompilujące po jednym pliku nakładają na niego ograniczenia.