在TypeScript中实现扩展了元数据的ES6 Map [英] Implementing ES6 Map augmented with metadata in TypeScript

查看:76
本文介绍了在TypeScript中实现扩展了元数据的ES6 Map的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我需要一个地图,其中的键不仅与值关联,而且还与某些元数据关联,例如"weight"或"timeToLive".

首先,一个用于保存不同权重的Map的示例界面:

 export interface Weighted {
    weight: number
}
 

...以及用于将所有元数据接口与实际值合并以保存在Map中的类型别名:

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

我拿了内置的Map并将其扩展.泛型类型:

  • 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];
}
 

解决方案

如果扩展Map<K, AugmValue<V, M>>,则您的扩展需要实现过载 方法,这样它仍然可以实现所需的签名,还可以接受附加的参数来提供额外的行为:

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") ...但是它具有简洁且正确键入的巨大优势. /p>


如果确实需要制作某种与基类不兼容的派生类(如果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";
}

这是一种痛苦,我想知道您是否应该使用一个Map<K, AugmValue<V, M>>而不是试图创建一个新的类?


无论如何,希望这些想法之一能为您提供帮助.祝你好运!

I need a Map where a key is associated not only with value but also with some metadata - like 'weight' or 'timeToLive'.

First, an example interface for Map holding items of different weights:

export interface Weighted {
    weight: number
}

...and a type alias to merge all the metadata interfaces with actual value to hold in a Map:

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

I took the built-in Map and extended it. As of generic types:

  • M is for metadata interfaces, like Weighted & Ttl
  • K is a type of keys
  • V is a type of actual values

... and implementation:

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;
    }
}

However, get gets underlined with lenghty message ending with:

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

How to correctly implement it? It is simplified case, eventually I'd like to have get() method like so:

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];
}

解决方案

If you extend Map<K, AugmValue<V, M>>, your extension needs to implement that interface, meaning that get(key: K) needs to return AugmValue<V, M>> | undefined, not V | undefined.

One possibility available to you is to overload the get() method so that it still implements the required signature but also accepts an additional argument to give the extra behavior:

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

This has the (possibly minor) drawback that get(key) doesn't return what you were hoping, and you need to call get(key, "value") instead... but it has the huge advantage of being terse and properly typed.


If you do really need to make some sort of derived class that is incompatible with the base class (if get(key) needs to return a V | undefined), then you can't use extends, since you are not producing a valid subtype. In such situations usually the advice is that your new class instance should have an instance of the base class, instead of trying to be an instance of the base class. This is called composition over inheritance. This is conceptually clear but in practice pretty annoying to implement, since it ends up mostly forwarding methods to the held instance:

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";
}

That's such a pain that I wonder if you should just use a Map<K, AugmValue<V, M>> instead of trying to create a new class at all?


Anyway, hope one of those ideas gives you some help. Good luck!

这篇关于在TypeScript中实现扩展了元数据的ES6 Map的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆