01 · Problem
Wofür braucht man das?
Beim Arbeiten mit Maps wiederholen sich Muster wie get-transform-set, selektives Kopieren und Konfliktauflösung beim Zusammenführen.
02 · Denkmodell
Das mentale Modell
Map bleibt die Datenstruktur; Helfer sollten lediglich atomare Collection-Operationen ausdrücken und klar sagen, ob sie die Instanz mutieren oder eine neue erzeugen.
BetterMap erbt von Map. update und merge mutieren, filter erzeugt eine neue BetterMap; ein fehlender Schlüssel kann trotz Typ V als undefined beim Updater ankommen.
03 · Kontrollfluss
Was passiert in welcher Reihenfolge?
- Mit has zwischen fehlendem Schlüssel und vorhandenem undefined unterscheiden.
- Vorhandenen Wert aktualisieren oder einen expliziten Initialwert erzeugen.
- Das Ergebnis genau einmal per set speichern.
- Bei Merge-Konflikten eine dokumentierte Resolver-Funktion anwenden.
04 · Bauteile
Die entscheidenden Verträge
update(key, updater, notset?)
Transformiert einen Wert am Schlüssel oder legt ihn an.
filter(predicate)
Erzeugt eine neue BetterMap aus passenden Einträgen.
merge(map, resolve?)
Führt Einträge mutierend zusammen und löst Konflikte optional per Callback.
05 · Build it yourself
Selbst implementieren
Der wichtigste Entwurfspunkt ist nicht die Vererbung, sondern eine eindeutige Abwesenheits- und Mutationssemantik.
- Prüfe Existenz immer mit has statt über den gelesenen Wert.
- Trenne create und update, damit undefined nicht als versteckter Initialwert dient.
- Lass mutierende Methoden this zurückgeben oder benenne immutable Varianten ausdrücklich.
class UpdatingMap<K, V> extends Map<K, V> {
update(key: K, change: (value: V) => V, create: () => V): this {
const next = this.has(key) ? change(this.get(key) as V) : create();
this.set(key, next);
return this;
}
select(predicate: (value: V, key: K) => boolean) {
const result = new UpdatingMap<K, V>();
for (const [key, value] of this) {
if (predicate(value, key)) result.set(key, value);
}
return result;
}
}06 · Verifizieren
Was du testen solltest
- Fehlender Schlüssel, vorhandenes undefined und vorhandenes null bleiben unterscheidbar.
- Filter verändert die Quelle nicht und erhält Schlüssel-Wert-Paare.
- Merge-Resolver wird ausschließlich bei echten Konflikten aufgerufen.
07 · Grenzen
Kompromisse und Stolperfallen
- Unterklassen nativer Collections können bei Serialisierung und Cross-Realm-Nutzung überraschen.
- Mutierende und immutable Operationen in derselben API brauchen besonders klare Namen.
Ein optionaler notset-Wert mit Nullish-Coalescing kann absichtlich gesetztes null oder undefined nicht ausdrücken.
08 · Weiterdenken