Skip to content

Instantly share code, notes, and snippets.

@rauschma
Last active Aug 22, 2021
Embed
What would you like to do?
/**
* 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