A proposal to add a take(key) method to Map and WeakMap that removes the entry for key and returns its value (or undefined if the key was absent) in a single operation.
- Stage: 0 (sketch / pre-proposal)
- Champion: Devin Rousso
- Authors: Devin Rousso
"Read a value and remove it in the same step" is one of the most common things people do with a map (e.g. consume a pending callback keyed by request id, pop a job off a work queue, evict and inspect a cache entry, hand off ownership of a buffered chunk, drain a "waiting for X" table, etc.).
JavaScript can express every other member of the CRUD quartet as a single call that returns something useful (e.g. map.get(k), map.set(k, v) (returns the map), and map.has(k)) except the "remove and read" combination.
Map.prototype.delete returns a boolean telling you whether anything was removed (i.e. it throws the value away).
So today you must write the value-preserving version by hand, which requires two lookups of the same key:
function take(map, key) {
const value = map.get(key); // lookup #1
map.delete(key); // lookup #2
return value;
}This has real downsides:
- Two lookups instead of one. For hot paths (dispatch tables, per-frame caches, etc.) the extra lookup is pure overhead an engine could avoid if the operation were a single method.
- It's a footgun to inline.
const v = map.get(k); map.delete(k); return v;is easy to reorder incorrectly (i.e.deletebefore capturing the value) and thedeleteboolean result is silently discarded so linters can't help. - Everyone re-implements it. The helper is reinvented under a dozen names (e,g,
getAndDelete,getAndRemove,pop,pull,popEntry,take,fetch, etc.) in codebase after codebase.
Map.prototype.take(key) // the removed value, or undefined if key was absent
WeakMap.prototype.take(key) // the removed value, or undefined if key was absenttake(key) is defined to be observably equivalent to reading get(key) and then performing delete(key), returning the value that get would have returned.
It mutates the map in place and returns the value directly (i.e. it does not return the map) because the whole point is to hand the value back to the caller.
// Consume request.
const resolveForRequestId = new Map();
function onResponse(id, payload) {
const resolve = resolveForRequestId.take(id); // grab it and clear it in one step
resolve?.(payload);
}
// Move ownership.
const buffers = new Map();
// ...
const chunk = buffers.take(streamId); // caller now owns `chunk`
// One-off logic.
const stateForObject = new WeakMap();
function consume(obj) {
const state = stateForObject.take(obj);
return state;
}| Situation | take(key) returns |
Side effect |
|---|---|---|
key present with value v |
v |
entry removed |
key present with value undefined |
undefined |
entry removed |
key absent |
undefined |
none |
WeakMap and key cannot be held weakly |
undefined |
none |
this is not a Map/WeakMap |
— | throws TypeError |
Like get, a return of undefined is ambiguous between "absent" and "present with value undefined".
If you need to tell them apart then you can always call has(key) first.
This mirrors get exactly, so it adds no new ambiguity to the language.
"Remove and return the value" is the default shape of this operation in most languages' standard libraries.
JavaScript is the outlier in returning only a boolean from delete.
| Language | API | Removes & returns value? | Notes |
|---|---|---|---|
| Rust | HashSet::take(&value) -> Option<T> |
✅ | Direct precedent for the name take: "removes and returns the value in the set, if any". |
| Rust | HashMap::remove(&k) -> Option<V> / remove_entry |
✅ | Returns the value (or the (k, v) pair). |
| Python | dict.pop(key[, default]) |
✅ | Returns value; raises KeyError if absent and no default given. |
| Ruby | Hash#delete(key) |
✅ | Returns the value (or nil); optional block for the missing case. |
| Java | Map.remove(Object key) -> V |
✅ | Returns the previous value, or null. |
| C# | Dictionary.Remove(key, out value) -> bool |
✅ | The out overload (added in .NET Core 3.0) hands back the removed value. |
| Swift | Dictionary.removeValue(forKey:) -> Value? |
✅ | Returns the removed value, or nil. |
| Kotlin | MutableMap.remove(key): V? |
✅ | Returns the previous value, or null. |
| C++ | std::map::extract / unordered_map::extract (C++17) |
✅ | Removes and hands back a node handle owning the key/value. |
| Perl | delete $hash{$key} |
✅ | The delete operator returns the deleted value. |
| Go | delete(m, k) |
❌ | No return; you must write v, ok := m[k]; delete(m, k) — the same two-step JS is stuck with. |
| JavaScript | Array.prototype.pop() -> element |
✅ | JS's existing remove-and-return: pops the last element and returns it (or undefined). take is the by-key analogue for maps. |
| JavaScript (today) | Map.prototype.delete -> boolean |
❌ | Returns only whether something was removed; the value is discarded. |
take is not a foreign shape for JavaScript as the language already has a "remove an element and return its value" method in Array.prototype.pop.
It mutates the collection in place, returns the removed element, and yields undefined when there is nothing to remove.
take applies that exact contract to keyed collections (i.e. addressed by key instead of by position):
Array.prototype.pop() |
Map.prototype.take(key) |
|
|---|---|---|
| Selects what to remove by | position (last index, LIFO) | key |
| Argument | none | the key |
| Returns | the removed element | the removed value |
| When nothing matches | undefined (empty array) |
undefined (absent key) |
| Mutates in place | ✅ | ✅ |
| Returns the container for chaining | ❌ (returns the element) | ❌ (returns the value) |
Because pop already establishes "mutate and hand back the value" as an idiomatic JavaScript pattern, take is a small, consistent extension of it rather than a new concept (i.e. it just removes by key).
takematches Rust'sHashSet::take("remove and return") and reads naturally "take the value out of the map".popis misleading as `Array.prototype.pop takes no argument and removes from the end (i.e. LIFO) instead of by index.removewould sit confusingly beside the existingdelete(i.e. two spellings with different return types) and collides with the many userlandremovemethods.
getAndDelete is an equally acceptable name.
Its advantage is that it extends the existing "family" of get, getOrInsert, getOrInsertComputed, etc. so the read-then-mutate relationship is explicit and greppable (i.e. getAndDelete reads literally as "get and then delete").
Counts below come from GitHub
GET /search/codewhich is a token-based index, covers only the default branch of public repos, and does not verify that the matched tokens act on the same key/variable.
| Query | Files |
|---|---|
"getAndDelete(" language:TypeScript |
~260 |
"getAndDelete(" language:JavaScript |
~281 |
"popEntry(" in:file (JS/TS) |
~3,270 |
getAndRemove in:file (all langs, token match) |
~48,000 |
| Query | Files |
|---|---|
cache.get cache.delete language:TypeScript |
~143,000 |
map.get map.delete language:JavaScript |
~99,800 |
"get(key)" "delete(key)" language:TypeScript |
~44,600 |
// pull out the pending request/callback when its response arrives
let responseData = this._pendingResponses.take(sequenceId) || {};
// remove and read a worker as it goes away
let worker = this._workerForId.take(workerId);
// adopt resources that were buffered for a target
let resources = this._orphanedResources.take(target.identifier);takevsgetOrDeletevs something else?- This sketch returns
undefined, mirroringget, sotakeis a drop-in replacement for thegetanddeletepair, but an alternative could be a Python-style optional defaulttake(key, defaultValue). - Is it worth adding
Set.prototype.takeand/orWeakSet.prototype.takefor parity like howSet.prototype.entriesreturns a two-value array to matchMap.prototype.entries?
'use strict';
(function installMapTakePolyfill() {
function defineTake(Ctor) {
if (typeof Ctor !== 'function' || typeof Ctor.prototype.take === 'function')
return;
const get = Ctor.prototype.get;
const del = Ctor.prototype.delete;
function take(key) {
const value = get.call(this, key);
del.call(this, key);
return value;
}
Object.defineProperty(Ctor.prototype, 'take', {
value: take,
writable: true,
enumerable: false,
configurable: true,
});
}
defineTake(typeof Map === 'function' ? Map : undefined);
defineTake(typeof WeakMap === 'function' ? WeakMap : undefined);
})();