Одновременно работая над JavaScript и Typescript, мне не хватает некоторых основных функций Typescript в обычном JavaScript. Одна из интересных особенностей Typescript - Enum.

Enum помогает создать объект состояния (Enums: константа с фиксированными значениями).

Подумайте о том, что вы работаете над приложением для загрузки файлов. Для вашего приложения-загрузчика необходимо как минимум три состояния.

  1. НАЧАЛО
  2. INPROGRESS
  3. ЗАВЕРШЕНО

Да, вы можете создать Map<number, any>::map[object]`. Карта будет выглядеть так:

const STATES = {
  START: 0,
  INPROGRESS: 1,
  FINISHED: 3,
};

Хорошее начало! Но по какой-то причине мы хотим запустить объект STATES с “1” вместо “0”. Давай изменим это.

const STATES = {
  START: 1,
  INPROGRESS: 2,
  FINISHED: 3,
};

Выполнено! Это выглядит мило. Однако нам пришлось изменить 3 значения. Здесь, в этом коде, все еще управляемо.

Здесь возникает новое требование. Теперь у нас есть значение состояния, и нам нужно найти текущий state name.

Код:

const STATES = {
  START: 1,
  INPROGRESS: 2,
  FINISHED: 3,
};
const getState = (val) =>
  Object.entries(STATES).find(([_, value]) => value === val)[0];
const currentState = 2; // equivalent of INPROGRESS
console.log(getState(currentState)); // INPROGRESS
// Output: INPROGRESS

Работает нормально! Не правда ли! Но подождите, мы забываем протестировать API. Давай проверим.

Примечание: я использую узел js assert module.

const assert = require("assert");
const STATES = {
  START: 1,
  INPROGRESS: 2,
  FINISHED: 3,
};
const getState = (val) =>
  Object.entries(STATES).find(([_, value]) => value === val)[0];
assert.equal(getState(2), "INPROGRESS") ||
  console.log(`getState(2) === "INPROGRESS" // PASS`);
// try again with negative test
assert.equal(getState(4), undefined) ||
  console.log(`getState(2) === undefined // PASS`);

Вывод:

getState(2) === "INPROGRESS" // PASS
/Users/xdeepakv/LearnAndShare/learn-js/plain-html/tempCodeRunnerFile.js:8
  Object.entries(STATES).find(([_, value]) => value === val)[0];
                                                            ^
TypeError: Cannot read property '0' of undefined

Первое утверждение работает нормально. Однако второе утверждение неверно. Причина, по которой мы забыли добавить null/undefined проверяемую находку. давай исправим это.

//.. rest of the code
const getState = (val) => {
  const obj = Object.entries(STATES).find(([_, value]) => value === val) || [];
  return obj[0];
};
assert.equal(getState(2), "INPROGRESS") ||
  console.log(`getState(2) === "INPROGRESS" // PASS`);
// Output: getState(2) === "INPROGRESS" // PASS
// try again with negative test
assert.equal(getState(4), undefined) ||
  console.log(`getState(2) === undefined // PASS`);
//Output: getState(2) === undefined // PASS

Все тестовые случаи проходят. Однако нам пришлось написать много кода, чтобы простая вещь работала.

Для оптимизации вы можете сказать, что мы можем создать map/object ключей и значений при создании впервые. Ага! Давай сделаем это.

//.. rest of the code..
const reveseState = Object.keys(STATES).reduce((m, key) => {
  m[STATES[key]] = key;
  return m;
}, {});
const getState = (val) => reveseState[val];
assert.equal(getState(2), "INPROGRESS") ||
  console.log(`getState(2) === "INPROGRESS" // PASS`);
// try again with negative test
assert.equal(getState(4), undefined) ||
  console.log(`getState(2) === undefined // PASS`);

Все снова хорошо. Но по-прежнему нужно управлять большим количеством кода. Нам пришлось создать карту reverseState, перебирая все значения. Но мы можем убрать это больше.

Перенесем это в класс.

const assert = require("assert");
class DownloadState {
  constructor() {
    const STATES = {
      START: 1,
      INPROGRESS: 2,
      FINISHED: 3,
    };
    this._reveseState = Object.keys(STATES).reduce((m, key) => {
      m[STATES[key]] = key;
      return m;
    }, {});
    this.STATES = STATES;
  }
  getState(val) {
    return this._reveseState[val];
  }
  values() {
    return this.STATES;
  }
}

Как использовать

const downlaodState = new DownloadState();
const { START } = downlaodState.STATES;
assert.equal(START, 1) || console.log(`START === 1 // PASS`);
// Output: START === 1 // PASS
assert.equal(downlaodState.getState(2), "INPROGRESS") ||
  console.log(`getState(2) === "INPROGRESS" // PASS`);
// Output: getState(2) === "INPROGRESS" // PASS

Теперь у нас есть класс DownloadState, и все работает как положено. Однако слишком много шаблонного кода, чтобы справиться только за one Enum. Если нам нужен другой Enum, похожий, но с другим значением. Нам нужно создать еще одну карту / класс / объект.

Давайте создадим utility функцию.

const enumVars = (args, startIndex = 1) => {
  return args.split("|").reduce((m, key, index) => {
    m[(m[key] = index + startIndex)] = key;
    return m;
  }, {});
};
const DOWNLOAD_STATES = enumVars("START|INPROGRESS|FINISHED", 1);
console.log(DOWNLOAD_STATES); 
// { '1': 'START', '2': 'INPROGRESS', '3': 'FINISHED', START: 1, INPROGRESS: 2, FINISHED: 3 }
const { START } = DOWNLOAD_STATES;
console.log(`CURR STATE is ${START}`)
// CURR STATE is 1
console.log(`CURR STATE is ${DOWNLOAD_STATES[2]}`)
// CURR STATE is INPROGRESS

Ага! Намного чище, чем все предыдущие реализации. Это даже может справиться с startIndex и reverseMapping тоже.

Но что, если я скажу, что мы можем очистить его еще с помощью Прокси. Давайте создадим Enum class.

class Enum {
  constructor(args, startIndex = 0, defaultValue) {
    const em = args.split("|").reduce((m, key, index) => {
      m[(m[key] = index + startIndex)] = key;
      return m;
    }, {});
    em.value = em.key = (k) => em[k];
    return new Proxy(em, {
      get: (obj, prop) => (prop in obj ? obj[prop] : defaultValue),
      set: (obj, prop, value) => {
        if (prop in obj) {
          throw new Error("cant set same value again"); //  // cant set same value again
        }
        em[(em[prop] = value)] = prop;
      },
    });
  }
}

Как использовать:

const DOWNLOAD_STATES = new Enum("START|INPROGRESS|FINISHED", 1);
console.log(JSON.stringify(DOWNLOAD_STATES));
//{"1":"START","2":"INPROGRESS","3":"FINISHED","START":1,"INPROGRESS":2,"FINISHED":3}
const { START } = DOWNLOAD_STATES;
console.log(`CURR STATE is ${START}`);
//Output: CURR STATE is 1

console.log(`CURR STATE is ${DOWNLOAD_STATES[2]}`);
//Output: CURR STATE is INPROGRESS

// ADD new State:
DOWNLOAD_STATES.IDLE = 4;
console.log(`CURR STATE is ${DOWNLOAD_STATES[4]}`);
//Output: CURR STATE is IDLE

ТАДА !! Все хорошо. Реализация выглядит более безопасной с awesome Proxy. Здесь мы также можем добавить новое состояние IDLE.

Вы можете найти последний рабочий код в моей сущности.

Бонус: машинописная версия

class Enum<Proxy> {
  readonly em: Map<string, number>;
  readonly defaultValue: any;
  constructor(args: string, startIndex = 0, defaultValue = undefined) {
    const em = args.split("|").reduce((m, key, index) => {
      m[(m[key] = index + startIndex)] = key;
      return m;
    }, {} as any);
    em.value = em.key = (k: string) => em[k];
    this.em = em;
    this.defaultValue = defaultValue;
  }
  build(): any {
    return new Proxy<Map<string, number>>(this.em, {
      get: (obj, prop) => (prop in obj ? obj[prop] : this.defaultValue),
      set: (obj, prop, value) => {
        if (prop in obj) {
          throw new Error("cant set same value again"); //  // cant set same value again
        }
        this.em[(this.em[prop] = value)] = prop;
        return true;
      },
    });
  }
}
// create insatnce using build
const DownloadState = new Enum<any>("START|DONE", 10).build();
console.log(DownloadState.START);

Надеюсь, это руководство помогло вам лучше понять Enum! Спасибо. 🙏

Продолжайте кодировать !! Ваше здоровье! 🍻🍻