Zrozumieć enumy w TypeScriptcie

Enum w TypeScriptcie to jednocześnie wygodne rozwiązanie jak i gruby problem. Szczególnie, że po kompilacji stają się komicznymi obiektami (albo - co gorsza - znikają), a ich interfejs jest bardzo ubogi.

Co to jest enum? Polskie tłumaczenie jest niezwykle wdzięczne... wyliczeniowy typ danych. A tak naprawdę to lista wartości.

Na początek zadeklarujmy sobie enum, żeby połączyć słowo ze składnią:

enum STORY_TYPE {
    MULTICHAPTER,
    ONESHOT,
    SERIES
} 

Enumy często opisują jakieś wartości poprawiając czytelność kodu. Mamy tutaj 3 typy opowiadania: wielorozdziałowe, pojedyncze oraz seria.

Wyobrażam sobie teraz kilka scenariuszy, gdzie możemy użyć tego typu danych:

Jako typ opowieści w klasie

Pewnie podstawowym użyciem będzie zdefiniowanie typu historii w klasie.

class Story {
    public storyType: STORY_TYPE;
}

Żeby potem móc tego użyć w tej sposób:

const myStory = new Story();
myStory.storyType = STORY_TYPE.ONESHOT;

A potem porównywać:

if (myStory.storyType === STORY_TYPE.MULTICHAPTER) {
	// zrób cośtam
}

Jako lista w filtrze

Załóżmy, że mamy filtry na liście opowiadań i chcemy zrobić dropDowna z możliwością wyboru jakieś typu.

for (let value in STORY_TYPE) {
    console.log(STORY_TYPE[value]);
}

Wynik:

// 0
// 1
// 2
// MULTICHAPTER
// ONESHOT
// SERIES

Ups! Nie o to chodziło :)

To może coś takiego:

console.log(Object.keys(STORY_TYPE));

Niestety nie, klątwa Javascriptu nadal poniewiera tym kodem:

Array(6) [ "0", "1", "2", "MULTICHAPTER", "ONESHOT", "SERIES" ] 

Dlaczego tak się dzieje? Może zajrzyjmy do źródła, czyli do wygenerowanego kodu:

"use strict";
var STORY_TYPE;
(function (STORY_TYPE) {
    STORY_TYPE[STORY_TYPE["MULTICHAPTER"] = 0] = "MULTICHAPTER";
    STORY_TYPE[STORY_TYPE["ONESHOT"] = 1] = "ONESHOT";
    STORY_TYPE[STORY_TYPE["SERIES"] = 2] = "SERIES";
})(STORY_TYPE || (STORY_TYPE = {}));

No dobrze, widać to wyraźnie, tylko co z tym można zrobić?

W sumie nic.

Jeśli wiecie taki to typ enuma: numeryczny, stringowy albo mieszany to można coś poradzić dodatkowymi funkcjami lub biblioteczkami.

for (let value in STORY_TYPE) {
	if (!Number(value)) {
    	console.log(STORY_TYPE[value]);
    }
}

Działa, ale nie wygląda zbyt ładnie...

"Typy" enumów

enum STORY_TYPE {
    MULTICHAPTER = 0,
    ONESHOT = 1,
    SERIES = 2
} 

Jeśli zadeklarujemy enuma w powyższy sposób, to wynik będzie identyczny co bez przypisania wartości do kluczy. Potocznie nazywa się to numerycznym enumem.

String enum

Kolejny typ to... wyliczeniowy typ danych literału łańcuchowego. Musicie przyznać, że to tłumaczenie brzmi pięknie :)

Ale żarty na bok, oto enum string:

enum STORY_TYPE {
    MULTICHAPTER = 'multichaper',
    ONESHOT = 'oneshot',
    SERIES = 'series'
} 

A teraz pokażę wam jak się po nim iteruje:

for (var t in STORY_TYPE) {
    console.log(t)
}

A tutaj wynik:

MULTICHAPTER
ONESHOT
SERIES

Widzicie to już? Nie trzeba robić żadnego sprawdzania, nie generują się jednocześnie klucze i wartości. Jak to się stało?

Wynika to ściśle ze sposobu w jaki TypeScript transpiluje się do JavaScript.

Porównanie transpilacji string i numeric enuma

To napiszmy dwa enumy i jakie są tam różnice:

enum STRING_ENUM {
    MULTICHAPTER = 'multichaper',
    ONESHOT = 'oneshot',
    SERIES = 'series'
} 

enum NUMERIC_ENUM {
    MULTICHAPTER,
    ONESHOT,
    SERIES,
} 

Transpilacja w toku...

"use strict";
var STRING_ENUM;
(function (STRING_ENUM) {
    STRING_ENUM["MULTICHAPTER"] = "multichaper";
    STRING_ENUM["ONESHOT"] = "oneshot";
    STRING_ENUM["SERIES"] = "series";
})(STRING_ENUM || (STRING_ENUM = {}));

var NUMERIC_ENUM;
(function (NUMERIC_ENUM) {
    NUMERIC_ENUM[NUMERIC_ENUM["MULTICHAPTER"] = 0] = "MULTICHAPTER";
    NUMERIC_ENUM[NUMERIC_ENUM["ONESHOT"] = 1] = "ONESHOT";
    NUMERIC_ENUM[NUMERIC_ENUM["SERIES"] = 2] = "SERIES";
})(NUMERIC_ENUM || (NUMERIC_ENUM = {}));

STRING_ENUM to zwykły obiekt w którym klucze enuma są kluczami obiektu, a wartość klucza enuma to wartość propsa obiektu. Gdy zobaczymy go w konsoli to:

{
  "MULTICHAPTER": "multichaper",
  "ONESHOT": "oneshot",
  "SERIES": "series"
}

Aby NUMERIC_ENUM działał w obie strony zarówno klucze jak i wartości enuma muszą być ze sobą powiązane.

{
  "0": "MULTICHAPTER",
  "1": "ONESHOT",
  "2": "SERIES",
  "MULTICHAPTER": 0,
  "ONESHOT": 1,
  "SERIES": 2
}

Z czego to wynika? Z decyzji twórców. Moim zdaniem finalnie bardzo dobrej. Żeby TypeScript był spójny pomimo takich kwiatków.

Podsumowanie

Podobało Wam się? Czy lubicie enumy czy jednak macie inne sposoby na modelowanie podobnych przypadków?

Powoli będę przed wami odsłaniała tajemnice TypeScripta, więc... stay tuned!

Kamila

Dziękuję za poświęcony czas, będzie mi bardzo miło jak zostawisz komentarz :)