возможно ли создать динамические свойства для класса?

#javascript #typescript #enums

#javascript #typescript #перечисления

Вопрос:

Я помещаю перечисление в класс, чтобы я мог сопоставить его значения в TypeScript

 enum Color {
    RED = 0,
    GREEN = 1,
    BLUE = 2
}

const MapColor = new EnumMap(Color); // Wrapper

console.log(MapColor.map()) // [{ "key": "RED", "value": 0}, { "key": "GREEN","value": 1}, { "key": "BLUE",  "value": 2}] 
console.log(MapColor.entries()) // [["RED", 0], ["GREEN", 1], ["BLUE", 2]] 
console.log(MapColor.keys()) // ["RED", "GREEN", "BLUE"] 
console.log(MapColor.values()) //  [0, 1, 2] 
 

Класс-оболочка

 class EnumMap {
  private _map = new Map<string, string | number>();
  public enum: any;

  constructor(enumeration: any) {
    this.init(enumeration);
    return this;
  }

  private init(enumeration: any) {
    this._map = new Map<string, string | number>();

    for (let key in enumeration) {
      if (!isNaN(Number(key))) continue;
      const val = enumeration[key] as string | number;
      if (val !== undefined amp;amp; val !== null) this._map.set(key, val);
    }

    this.enum = enumeration;
  }

  map = (): Object => Array.from(this._map.entries()).map((m) => ({ key: m[0], value: m[1] }));
  entries = (): any[] => Array.from(this._map.entries());
  keys = (): any[] => Array.from(this._map.keys());
  values = (): any[] => Array.from(this._map.values());
}
 

Я хотел бы получить прямой доступ к значению enum, я хочу сделать это

 MapColor.RED  // 0
MapColor.RED.toString()  // 'RED'
 

Возможно ли создать динамические свойства для класса? Если возможно, как это будет сделано?

Комментарии:

1. Вполне возможно, используя дженерики, но зачем вам это нужно?

2.Я хочу сделать это MapColor.RED // 0 MapColor.RED.toString() // 'RED'

3. Да, это можно сделать, но использовать для этого класс бессмысленно. Фабричную функцию, которая возвращает объект-оболочку, намного проще написать, а типы менее сложны.

4. Подождите секунду, забудьте о классах на минуту. Вы хотите MapColor.RED быть примитивным числовым значением 0 и хотите MapColor.RED.toString() вывести строку "RED" ? Например, вы хотите MapColor.RED === Color.RED быть true , но String(MapColor.RED) быть "RED" , верно? Невозможно.

5. Я собираюсь полностью игнорировать невозможное редактирование вопроса, потому что в противном случае ответ будет просто «нет», и я уже начал писать ответ на возможную версию этого 😛

Ответ №1:

В СТОРОНУ:

Я игнорирую запрос, который MapColor.RED должен быть 0 , но MapColor.RED.toString() должен оцениваться "RED" . Если вы хотите MapColor.RED === Color.RED быть true , так что MapColor.RED это number (примитивный тип данных), значение которого равно 0 , вам совершенно невозможно получить "RED" в качестве его строкового значения, поскольку (0).toString() это просто "0" . Если вы хотите MapColor.RED быть Number (объектом-оболочкой), вы могли бы сделать это как Object.assign(Number(0), {toString(){return "RED"}) , но то, что из этого получается, ужасно. Он действует как 0 ( MapColor.RED 5 === 5 ) , за исключением случаев, когда это не так, поскольку MapColor.RED === 0 is false и someObject[MapColor.RED] будет обращаться к RED свойству someObject , а не к 0 свойству. Таким образом, вы либо не можете этого сделать, либо можете, но не должны. Поэтому я притворяюсь, что его не существует. 🙈


ОТВЕТ:

Вы можете реализовать такой класс, скопировав свойства enumeration непосредственно this внутри конструктора (или init() метода, я полагаю). И вы можете описать тип такого конструктора класса, используя обобщения через пересечение. Чего вы не можете сделать, так это заставить компилятор проверить, что реализация соответствует описанию. Поэтому вам нужно будет использовать утверждения типа в нескольких местах, чтобы сообщить компилятору, что ваши экземпляры класса и конструктор класса соответствуют рассматриваемому поведению:

 class _EnumMap<T extends Record<keyof T, string | number>> {
    private _map = new Map<keyof T, T[keyof T]>();
    public enum!: T;

    constructor(enumeration: T) {
        this.init(enumeration);
        return this;
    }

    private init(enumeration: T) {
        this._map = new Map<keyof T, T[keyof T]>();

        for (let key in enumeration) {
            if (!isNaN(Number(key))) continue;
            const val = enumeration[key];
            if (val !== undefined amp;amp; val !== null) {
                this._map.set(key, val);
                (this as any)[key] = val; // set value here
            }
        }

        this.enum = enumeration;
    }

    map = () => Array.from(this._map.entries())
        .map((m) => ({ key: m[0], value: m[1] })) as
        Array<{ [K in keyof T]: { key: K, value: T[K] } }[keyof T]>;
    entries = () => Array.from(this._map.entries()) as
        Array<{ [K in keyof T]: [K, T[K]] }[keyof T]>;
    keys = () => Array.from(this._map.keys()) as Array<keyof T>;
    values = () => Array.from(this._map.values()) as Array<T[keyof T]>;
}
 

Я переименовал EnumMap путь в _EnumMap , и я восстанавливаю EnumMap имя в конце:

 type EnumMap<T extends Record<keyof T, string | number>> = _EnumMap<T> amp; T;
const EnumMap = _EnumMap as 
  new <T extends Record<keyof T, string | number>>(enumeration: T) => EnumMap<T>;
 

Переименованный EnumMap является общим по типу T enumeration переданного ему объекта. Внутри init() я пишу this[key] = val , но компилятор не знает, что this у него должны быть ключи, соответствующие key , поэтому я утверждаю this as any , чтобы ослабить предупреждения компилятора. Я также изменил типы вывода всех ваших существующих методов, чтобы более точно соответствовать тому, что вы получаете от них… но вы не просили об этом, поэтому сохраните any и Object (или, возможно object ), если необходимо.

Написав type EnumMap<T> = _EnumMap<T> amp; T , я говорю, что an EnumMap<T> действует как ваш исходный класс, так и как T . Итак, если T выглядит примерно так {RED: 0, GREEN: 1, BLUE: 2} , то EnumMap<T> также будет иметь эти свойства. И, записывая const EnumMap = _EnumMap as new<T>(enumeration: T) => EnumMap<T> , я говорю, что исходный конструктор класса был скопирован в значение с именем EnumMap и может обрабатываться как конструктор класса для экземпляров new EnumMap<T> .


Давайте протестируем это:

 enum Color {
    RED = 0,
    GREEN = 1,
    BLUE = 2
}

const MapColor = new EnumMap(Color); // Wrapper

console.log(MapColor.map()) // [{ "key": "RED", "value": 0}, 
  // { "key": "GREEN","value": 1}, { "key": "BLUE",  "value": 2}] 
console.log(MapColor.entries()) // [["RED", 0], ["GREEN", 1], ["BLUE", 2]] 
console.log(MapColor.keys()) // ["RED", "GREEN", "BLUE"] 
console.log(MapColor.values()) //  [0, 1, 2] 
console.log(MapColor.RED) // 0
console.log(MapColor.GREEN) // 1
console.log(MapColor.BLUE) // 2
 

Для меня это выглядит хорошо.


Игровая площадка ссылка на код