Menu

Enum w TypeScript: enumy numeryczne, tekstowe i const enum

Enum w TypeScript to nazwany zbiór stałych, na przykład enum Direction { Up, Down }. Poznaj enumy numeryczne i tekstowe, JavaScript, do którego kompiluje się enum, mapowanie odwrotne, iterowanie po enumie, const enum oraz sytuacje, w których lepiej sprawdza się unia literałów tekstowych albo obiekt as const.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

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:

enumUnia literałówObiekt as const
Istnieje w czasie działaniatak, jako obiektnietak, jako zwykły obiekt
Iterowanie po wartościachObject.values (numeryczny: z filtrem)nie, nie ma po czymObject.values
Przyjmuje zwykłe "red"nie (enumy tekstowe)taktak
Dostęp po nazwie X.Redtaknietak
Mapowanie odwrotnetylko enumy numerycznenienie
Działa z usuwaniem typów w Nodenietaktak
Dozwolony przy erasableSyntaxOnlynietaktak
Dodatkowa składnia do naukireguły enumów, const enumyżadnawzorzec 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.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ