Default Map in JavaScript

snippet

I’ve always liked Python’s defaultdict and occasionally find myself reimplementing it in other languages, notably JavaScript – typically for some kind of dynamic indexing.

NB:

Note that these days, there are standardized equivalents with getOrInsert and getOrInsertComputed. While our custom implementation here defines the initializer at the time of instantiation, those standardized methods defer to the time of access.

let index = new DefaultMap(() => []);
// …
for(let category of ["good", "bad", "ugly"]) {
    let values = index.get(category);
    values.push(/* … */);
}

That’s often much cleaner than using a regular Map and branching inline (because separation of concerns, plus it’s easy to make subtle mistakes):

let index = new Map();
// …
for(let category of ["good", "bad", "ugly"]) {
    let values = index.get(category);
    if(values === undefined) {
        values = [];
        index.set(category, values);
    }
    values.push(/* … */);
}

My implementation always ends up looking like this (reluctantly augmented with static types here, just in case):

/**
 * @template Key
 * @template Value
 * @extends Map<Key, Value>
 */
class DefaultMap extends Map {
    #initializer;

    /** @param {(key: Key) => Value} initializer */
    constructor(initializer) {
        super();
        this.#initializer = initializer;
    }

    /**
     * @param {Key} key
     * @returns {Value}
     */
    get(key) {
        if(this.has(key)) {
            return /** @type {Value} */ (super.get(key));
        }

        let value = this.#initializer(key);
        this.set(key, value);
        return value;
    }
}