Map.prototype.at() & Set.prototype.at()
Stage: 0 (with a dream)
Author: Someone tired of converting Maps to arrays
Help wanted: Champion, spec writers, emotional support
The Pitch
Arrays and strings got .at() in ES2022. Negative indices. Clean syntax. No more arr[arr.length - 1] nonsense.
Maps and Sets are ordered too. They deserve the same dignity.
The Problem (with evidence)
const users = new Map([
[1, 'Alice'],
[2, 'Bob'],
[3, 'Charlie']
]);
// Current state of affairs (all terrible):
const last = [...users.values()].pop(); // O(n) memory + time
const lastAgain = Array.from(users).at(-1)[1]; // still O(n) memory
let lastManual;
for (const v of users.values()) lastManual = v; // O(n) time, but gross
// Meanwhile, in array-land:
const arr = ['Alice', 'Bob', 'Charlie'];
const elegant = arr.at(-1); // 'Charlie' — chef's kiss
Search data: "how to get last element of Map" has too many results for something this basic.
The Solution
const map = new Map([['a', 1], ['b', 2], ['c', 3]]);
const set = new Set(['red', 'green', 'blue']);
// Happy path
map.at(0); // 1 (first value)
map.at(-1); // 3 (last value — finally)
set.at(0); // 'red'
set.at(-1); // 'blue'
// Edge cases
map.at(99); // undefined (no crash, just vibes)
map.at(-99); // undefined
map.at('a'); // undefined (type checking)
Basic API usage
// Primary: just values (consistent with array/string)
Map.prototype.at(index) // returns value
Set.prototype.at(index) // returns value
// Secondary: when you need the key too
Map.prototype.entryAt(index) // returns [key, value] or undefined
Why this is better than the boolean flag:
•No polymorphic return types (TypeScript/type systems are happier)
•Clearer intent: map.entryAt(-1) vs map.at(-1, true)
•Easier to polyfill and remember
•Memory & Performance
| Method | Memory | Time | Clean Code |
|---|---|---|---|
[...map.values()].pop() |
O(n) | O(n) | no |
Array.from(map).at(-1) |
O(n) | O(n) | god no |
| Manual iteration | O(1) | O(n) | NO |
map.at(-1) |
O(1) | O(n) | yes |
| Same O(n) time. Zero O(n) memory. The array copy was the real crime. |
Real-World Usage
// Get last 5 notifications without O(n) memory explosion
const notifications = new Map(); // id -> notification
// ... thousands of notifications ...
const lastFive = [];
for (let i = -1; i >= -5 && notifications.at(i); i--) {
lastFive.unshift(notifications.at(i));
}
// Dashboard: first and last metrics
const metrics = new Map([['cpu', 42], ['memory', 86], ['disk', 33]]);
metrics.at(0); // 42 (current CPU)
metrics.at(-1); // 33 (disk usage)
// Pagination with Sets (unique active users)
const activeUsers = new Set(['alice', 'bob', 'charlie']);
const pageOne = [activeUsers.at(0), activeUsers.at(1), activeUsers.at(2)];
The Polyfill (production-ready)
// Map
if (!Map.prototype.at) {
Map.prototype.at = function(index) {
if (typeof index !== 'number') return undefined;
let target = index >= 0 ? index : this.size + index;
if (target < 0 || target >= this.size) return undefined;
let i = 0;
for (const value of this.values()) {
if (i++ === target) return value;
}
};
}
if (!Map.prototype.entryAt) {
Map.prototype.entryAt = function(index) {
if (typeof index !== 'number') return undefined;
let target = index >= 0 ? index : this.size + index;
if (target < 0 || target >= this.size) return undefined;
let i = 0;
for (const entry of this) {
if (i++ === target) return entry;
}
};
}
// Set
if (!Set.prototype.at) {
Set.prototype.at = function(index) {
if (typeof index !== 'number') return undefined;
let target = index >= 0 ? index : this.size + index;
if (target < 0 || target >= this.size) return undefined;
let i = 0;
for (const value of this) {
if (i++ === target) return value;
}
};
}
FAQ (anticipating TC39 objections)
Q: Isn't O(n) bad?
A: Maps aren't arrays. If you need O(1) indexed access, use an array. This is for occasional "give me the first/last/nth item" without allocating a temporary array.
Q: Why not just use Map.prototype.first and Map.prototype.last?
A: Because at(-2) exists. Because symmetry with arrays matters. Because developers already know .at().
Q: What about numeric keys?
A: .at() uses insertion order, not key lookup. map.get(3) is for keys. This is consistent with array behavior.
Q: This is just a utility function, not a language feature
A: So was Array.prototype.at before it landed. Sometimes the little quality-of-life wins matter most.
Q: Will this be used enough to justify spec time?
A: Every project I've worked on has a lastMapValue utility or inline [...map.values()].pop(). The array conversion guilt trip is real and widespread.
Why This Should Move Forward
Symmetry - Arrays and strings have it. Maps and Sets are ordered iterables.
Discoverability - .at() is already in every JS developer's mental model.
Memory safety - Eliminates unnecessary array allocations.
Developer happiness - Small wins add up.
The TL;DR
Arrays got nice things. Maps and Sets are ordered too. Give them .at() so I can stop explaining to juniors why map.get(map.size - 1) doesn't work and why [...map.values()].pop() is embarrassing.
Thank you for coming to my TC39 talk.