Реализация карты ES6, дополненной метаданными в TypeScript - PullRequest
0 голосов
/ 30 сентября 2018

Мне нужна карта, где ключ связан не только со значением, но и с некоторыми метаданными, такими как 'weight' или 'timeToLive'.

Во-первых, пример интерфейса для карты, содержащей элементы разного веса:

export interface Weighted {
    weight: number
}

... и псевдоним типа для объединения всех интерфейсов метаданных с фактическим значением для хранения на карте:

type AugmValue<V, M> = M & {
    value: V;
}

Я взял встроенную Карту и расширил ее.Что касается универсальных типов:

  • M для интерфейсов метаданных, например Weighted & Ttl
  • K - это тип ключей
  • V - это тип фактических значений

... и реализация:

export class MdaMap<M, K, V> extends Map<K, AugmValue<V, M>> {
    constructor(records: [K, V, M][] = []) {
        const result: [K, AugmValue<V, M>][] = [];
        for (const [key, val, meta] of records) {
            result.push([key, Object.assign({value: val}, meta)]);
        }
        super(result);
    }

    get(key: K): V | undefined {
        const t = super.get(key);
        return typeof t === 'undefined' ? undefined : t.value;
    }
}

Однако get подчеркнута длинным сообщением, оканчивающимся на:

Type 'V' is not assignable to type 'AugmValue<V, M>'.
      Type 'V' is not assignable to type 'M'.

Как правильно его реализовать?Это упрощенный случай, в конце концов я хотел бы иметь метод get(), например:

get(key: K, meta: keyof AugmValue<V, M> = 'value'): AugmValue<V, M> [keyof M | "value"] | undefined {
    const t = super.get(key);
    return typeof t === 'undefined' ? undefined : t[meta];
}

1 Ответ

0 голосов
/ 01 октября 2018

Если вы расширяете Map<K, AugmValue<V, M>>, ваше расширение должно реализовать этого интерфейса , то есть get(key: K) должно возвращать AugmValue<V, M>> | undefined, а не V | undefined.

Одна доступная вам возможность - перегрузить метод get(), чтобы он все еще реализовывал требуемую сигнатуру, но также принимал дополнительный аргумент для предоставления дополнительного поведения:

export class MdaMap<M, K, V> extends Map<K, AugmValue<V, M>> {

  get(key: K): AugmValue<V, M> | undefined;
  get<AK extends keyof AugmValue<V, M>>(
    key: K, 
    augmentedValueKey: AK
  ): AugmValue<V, M>[AK] | undefined;
  get<AK extends keyof AugmValue<V, M>>(
    key: K, 
    augmentedValueKey?: AK
  ): AugmValue<V, M> | AugmValue<V, M>[AK] | undefined {
    const t = super.get(key);
    return typeof augmentedValueKey === 'undefined' ? t : 
      typeof t === 'undefined' ? undefined : t[augmentedValueKey];
  }

}

const mdaMap = new MdaMap([["a", true, { weight: 4 }], ["b", false, { weight: 17 }]]);
const augmented = mdaMap.get("a"); // AugmValue<boolean, Weighted> | undefined
const unaugmented = mdaMap.get("a", "value"); // boolean | undefined
const weight = mdaMap.get("a", "weight"); // number | undefined

У этого есть (возможно незначительный) недостаток, заключающийся в том, что get(key) не возвращает того, на что вы надеялись, и вам нужно вместо этого вызвать get(key, "value") ... но у него есть огромное преимущество - быть кратким и правильно набранным.


Если вам действительно нужно создать какой-то производный класс, несовместимый с базовым классом (если get(key) необходимо вернуть V | undefined), тогда вы не можете использовать extends, так как вы не производите действительный подтип.В таких ситуациях обычно советуют, чтобы ваш новый экземпляр класса имел экземпляр базового класса, вместо того, чтобы пытаться быть экземпляром базового класса.Это называется композиция над наследованием .Это концептуально ясно, но на практике это довольно раздражает в реализации, поскольку в конечном итоге это в основном направляет методы в удерживаемый экземпляр:

class OtherMap<M, K, V> {
  private innerMap: Map<K, AugmValue<V, M>> = new Map();
  constructor(records: [K, V, M][] = []) {
    this.innerMap = new Map(records.map(
      ([k, v, m]) => [k, Object.assign({ value: v }, m)] as [K, AugmValue<V, M>]
    ));
  }
  clear(): void {
    return this.innerMap.clear();
  }
  delete(key: K): boolean {
    return this.innerMap.delete(key);
  }
  forEach(callbackfn: (value: M & { value: V; }, key: K, map: OtherMap<M, K, V>) => void, thisArg?: any): void {
    return this.innerMap.forEach((v, k) => callbackfn(v, k, this), thisArg);
  }
  get(key: K): V | undefined {
    return (this.innerMap.get(key) || { value: undefined }).value;
  }
  has(key: K): boolean {
    return this.innerMap.has(key);
  }
  set(key: K, value: M & { value: V; }): this {
    this.innerMap.set(key, value);
    return this;
  }
  get size(): number {
    return this.innerMap.size;
  }
  [Symbol
    .iterator](): IterableIterator<[K, M & { value: V; }]> {
    return this.innerMap[Symbol.iterator]();
  }
  entries(): IterableIterator<[K, M & { value: V; }]> {
    return this.innerMap.entries();
  }
  keys(): IterableIterator<K> {
    return this.innerMap.keys();
  }
  values(): IterableIterator<M & { value: V; }> {
    return this.innerMap.values();
  }
  [Symbol.toStringTag]: "Map";
}

Это такая боль, что мне интересно, стоит ли просто использовать a Map<K, AugmValue<V, M>> вместо того, чтобы вообще пытаться создать новый класс?


В любом случае, надеюсь, что одна из этих идей поможет вам.Удачи!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...