Skip to content

Instantly share code, notes, and snippets.

@Akiyamka
Last active June 1, 2022 12:11
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Akiyamka/de16fc1f887e187258b657555f54506b to your computer and use it in GitHub Desktop.
Save Akiyamka/de16fc1f887e187258b657555f54506b to your computer and use it in GitHub Desktop.

Что не так с enum в typescript

Вводная

Я никак не могу обойти тему того чем является тайпскрипт, и чем он должен был быть.

"TypeScript is JavaScript with syntax for types." www.typescriptlang.org

Это именно то что отличает тайпскрипт он кофескриптов, эльмов, ризонов и т.д. Все что нужно чтобы получить javascript - убрать аннотации типов. В вебе где размер бандла одна из ключевых метрик это жизненно важная ключая особенность. Когда вы можете добавить типизацию и получить + 0кб в финальный бандл - это прекрасно и то как это должно работать. Никому не нужен лишний вес, верно? Однако это не всегда так работает, есть пару исключений когда тайпскрипт генерирует лишний код, и enum один из них.

Enum Генерирует (мусорный) код

Наглядный пример. На входе имеем

enum UserStatus {
    REGISTERED,
    INACTIVE,
    NOT_FOUND,
    BANNED
}

На выходе:

var UserStatus;
(function (UserStatus) {
    UserStatus[UserStatus["REGISTERED"] = 0] = "REGISTERED";
    UserStatus[UserStatus["INACTIVE"] = 1] = "INACTIVE";
    UserStatus[UserStatus["NOT_FOUND"] = 2] = "NOT_FOUND";
    UserStatus[UserStatus["BANNED"] = 3] = "BANNED";
})(UserStatus || (UserStatus = {}));

Что же делать? Например воспользуемся union

На входе:

type UserStatus = "registered" | "inactive" | "notFound" | "banned" 

На выходе:

// Nothing!

Вы можете возразить

Мне не нравиться пологаться на строки! Что если значение стркои поменяется, мне нужно будет изменить ее везде?

Я бы рекомендовал поменять значение стркои везде, нет ничего хорошего когда у одной и той же вещи два имени. Благо современные иде позволяют это сделать без проблем в полуавтоматическом режиме.

Впрочем, если очень хочется, никто не запретит вам создать обьект

const UserStatus = Object.freeze({
  REGISTERED: 'REGISTERED',
  INACTIVE: 'REGISTERED',
  NOT_FOUND: 'NOT_FOUND',
  BANNED: 'BANNED'
} as const);

Да, это больше букв в исходном коде, однако это именно то что попадет в финальный код, пропадет только as const, т.е. настоящий тайпскрипт как типы для js. И это все еще лучше чем то что генерирует enum. К тому же вы не увидите неожиданных цифр в рантайме в console.log.

Использует зарезервированное слово

Слово enum зарезервировано в js. Если мы хотим добавить в язык что-то мы не должны его ломать (включая его будущие версии, я надеюсь все тут понимают смысл слова "зарезервировано"). Использование зарезервированного слова под свои цели это потенциальный конфликт. Кто активно и давно пользовался декоратарами тот в теме какой болью это может обернуться в будущем, тут те же самые грабли по тем же самым лекалам. Надеетесь что в этот раз обойдется? У меня для вас плохие новости

Вы не можете использовать сравнение по значению

Typescript просто не умеет мапить энамымежду собой. Т.е. вот это с сточки зрения ts не валидно даже в том случае если вы прямо задекларируете что UserStatus.REGISTERED представлен в виде "registered" строки.

enum UserStatus {
    REGISTERED="registered",
    INACTIVE="inactive",
    NOT_FOUND="notFound",
    BANNED="banned"
}

if ("registered" === UserStatus.REGISTERED) {

}

Здесь мы получам false negative ошибку.

Ну зачем же мне так писать

Даже этот упрощенный пример имеет место быть в реальном коде. Однако на практике встречаютсья и более сложные кейсы. Больнее всего это ломает совместимость интерфейсов. Один модуль предоставляет интерфейс, другой ожидает, каждый из них использует свою декларацию типов. TS позволяет это делать ровно до тех пор пока вы не занесете enum в ваш код

enum UserStatus1 {
    REGISTERED="registered",
    UNREGISTERED="unregistered",
}

enum UserStatus2 {
    REGISTERED="registered",
    UNREGISTERED="unregistered",
}

type RegisteredUser = {
    status: UserStatus1.REGISTERED,
    email: string;
}

type AnonymusUser = {
    status: UserStatus1.UNREGISTERED,
    email: null;
}

type User = RegisteredUser | AnonymusUser;

const user: User  = {
    status: UserStatus2.UNREGISTERED,
    email: null
}

Все еще сомневаетесь?

(список будет пополняться)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment