Last active
August 22, 2021 15:07
-
-
Save rauschma/edeff456a9bda59f533f11faf0a389be to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* This module contains two data structures: | |
* | |
* - `KeyContainerMap` lets us use “key containers” as Map keys. | |
* - `KeyContainerSet` lets us use “key containers” as Set elements. | |
* | |
* A “key container” is an object that has a method `.getMapKey()`. Two key | |
* containers are considered equal if `.getMapKey()` returns the same value | |
* (often a string). | |
* | |
* **Use case:** In a Node.js script, we’d like to change how we represent file | |
* system paths and migrate from strings to instances of the class `NativePath`. | |
* | |
* - Problem we are facing now: We can’t use paths as keys in Maps anymore. | |
* - Solution: Use `KeyContainerMap<NativePath, string>` instead of | |
* `Map<NativePath, string>`. | |
* | |
* In TypeScript, `KeyContainerMap` is even interface-compatible with `Map`. | |
* Thus, if a parameter (etc.) expects `Map<K,V>`, we can pass it a | |
* `KeyContainerMap<K,V>`. The same works for `KeyContainerSet` and `Set`. | |
* | |
* @module | |
*/ | |
export interface KeyContainer<TKey> { | |
getMapKey(): TKey; | |
} | |
type GetMapKeyType<KC> = KC extends KeyContainer<infer TKey> ? TKey : never; | |
export class KeyContainerMap<TKeyContainer extends KeyContainer<TKey>, TValue, TKey=GetMapKeyType<TKeyContainer>> { | |
#map = new Map<TKey, [TKeyContainer, TValue]>(); | |
constructor(pairs: Iterable<[TKeyContainer, TValue]> = []) { | |
for (const [keyContainer, value] of pairs) { | |
this.set(keyContainer, value); | |
} | |
} | |
get(keyContainer: TKeyContainer): undefined | TValue { | |
const pair = this.#map.get(keyContainer.getMapKey()); | |
if (pair === undefined) { | |
return undefined; | |
} | |
return pair[1]; | |
} | |
set(keyContainer: TKeyContainer, value: TValue): this { | |
this.#map.set(keyContainer.getMapKey(), [keyContainer, value]); | |
return this; | |
} | |
delete(keyContainer: TKeyContainer): boolean { | |
return this.#map.delete(keyContainer.getMapKey()); | |
} | |
has(keyContainer: TKeyContainer): boolean { | |
return this.#map.has(keyContainer.getMapKey()); | |
} | |
* keys(): IterableIterator<TKeyContainer> { | |
for (const [key, _value] of this.#map.values()) { | |
yield key; | |
} | |
} | |
* values(): IterableIterator<TValue> { | |
for (const [_key, value] of this.#map.values()) { | |
yield value; | |
} | |
} | |
* entries(): IterableIterator<[TKeyContainer, TValue]> { | |
for (const pair of this.#map.values()) { | |
yield pair; | |
} | |
} | |
get size(): number { | |
return this.#map.size; | |
} | |
clear() { | |
this.#map.clear(); | |
} | |
forEach(callbackfn: (value: TValue, key: TKeyContainer, map: Map<TKeyContainer, TValue>) => void, thisArg?: any): void { | |
for (const [key, value] of this.#map.values()) { | |
callbackfn.call(thisArg, value, key, this); | |
} | |
} | |
[Symbol.iterator](): IterableIterator<[TKeyContainer, TValue]> { | |
return this.entries(); | |
} | |
/** | |
* Needed to be interface-compatible with `Map`. | |
*/ | |
get [Symbol.toStringTag](): string { | |
return 'KeyContainerMap'; | |
} | |
} | |
export class KeyContainerSet<TKeyContainer extends KeyContainer<TKey>, TKey = GetMapKeyType<TKeyContainer>> { | |
#map = new Map<TKey, TKeyContainer>(); | |
constructor(elements: Iterable<TKeyContainer> = []) { | |
for (const element of elements) { | |
this.add(element); | |
} | |
} | |
has(keyContainer: TKeyContainer): boolean { | |
return this.#map.has(keyContainer.getMapKey()); | |
} | |
delete(keyContainer: TKeyContainer): boolean { | |
return this.#map.delete(keyContainer.getMapKey()); | |
} | |
add(keyContainer: TKeyContainer): this { | |
this.#map.set(keyContainer.getMapKey(), keyContainer); | |
return this; | |
} | |
values(): IterableIterator<TKeyContainer> { | |
return this.#map.values(); | |
} | |
[Symbol.iterator](): IterableIterator<TKeyContainer> { | |
return this.values(); | |
} | |
get size(): number { | |
return this.#map.size; | |
} | |
clear(): void { | |
this.#map.clear(); | |
} | |
forEach(callbackfn: (value: TKeyContainer, value2: TKeyContainer, set: Set<TKeyContainer>) => void, thisArg?: any): void { | |
for (const value of this.#map.values()) { | |
callbackfn.call(thisArg, value, value, this); | |
} | |
} | |
/** | |
* Needed to be interface-compatible with `Map`. | |
*/ | |
get [Symbol.toStringTag](): string { | |
return 'KeyContainerSet'; | |
} | |
/** | |
* Needed to be interface-compatible with `Map`. | |
*/ | |
* entries(): IterableIterator<[TKeyContainer, TKeyContainer]> { | |
for (const value of this.values()) { | |
yield [value, value]; | |
} | |
} | |
/** | |
* Needed to be interface-compatible with `Map`. | |
*/ | |
keys(): IterableIterator<TKeyContainer> { | |
return this.#map.values(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment