Map and set getters to give a way to get an item at an order

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.

Also, sorry for the recent amount of posts Ive made in one day, I have been working on this single function and accidentally posted 2 other wrong plan files :/
sincerely sorry to the servers
By the way any questions asked in the comments will have to wait some time before they get answered by me because I'm busy with programming (a lot)

Related thread:

mapOrSet.values().drop(n).take(1).toArray()[0]?

That works, but it creates two intermediate iterators and an array for a single value. map.at(n) is O(n) with no temporary allocations. Your example allocates at least two iterators and an array — fine for small Maps, but for large ones (hundreds of thousands of entries), that's measurable memory pressure. Plus, drop(n) still iterates through all previous entries — same time complexity, worse memory profile.

Thanks for the real-world example! The SIEVE algorithm is exactly the kind of use case where map.entryAt(n) shines — avoiding a separate tracking array saves memory.*

To address your feasibility question: Map insertion order is tracked internally via a linked list of entries (or similar structure in engines like V8). The order is already there; we're just asking for read-only indexed access to that linked list. Engines can implement this by walking the list O(n) — same as [...map.keys()][n] but without allocating the intermediate array.

*So your .nth() example is entirely feasible today. Engines like V8 could optimize it further for repeated access patterns, but even a naive O(n) implementation would match what developers currently do — just with less memory pressure.

There's no requirement for engines to implement Maps or Sets so that it's actually O(1) for index access - theoretical big O isn't necessarily relevant. The only thing that's required to be nonlinear is key access.

What's your use case where you have large Maps and you need to access by index, but can't by key?

Cache eviction algorithms (LRU, SIEVE, FIFO) don't know the keys ahead of time. They need to find the 'nth oldest' entry based on insertion order, then evict it by key. That's exactly what the previous delegate showed with SIEVE. (The related thread claim)

Current workaround: const [key, value] = [...map.entries()][index] — which allocates a full array of all entries just to get one. The algorithm then discards that array. For large Maps (millions of entries), this is measurable memory churn and GC pressure.

.entryAt(n) would avoid that allocation. Same O(n) iteration under the hood (if engines choose), but no temporary array. The engine can walk the internal linked list until it reaches the nth node and return just that entry.

So the use case is: algorithms that need to examine entries by insertion order without knowing their keys. These exist today in caching libraries — and they all currently pay the spread-to-array tax.

Wouldn't a parallel array of keys give you this? A Map subclass could abstract over it.

A parallel array works, but it doubles memory usage (Map + array). For large caches with millions of entries, that's a real cost. It also requires manual synchronisation — every set/delete must update both structures, which adds maintenance overhead and bug surface.

The Map already tracks insertion order internally via a linked list. That memory is already allocated. entryAt(n) just exposes read-only access to that existing order — no extra memory, no sync bugs.

A subclass could work, but it can't avoid the memory overhead of the parallel array unless it accesses the internal linked list, which isn't exposed to user code. That's why this needs a spec change.

Yes, that works. But the proposal isn't about whether something works—it's about whether the ergonomic and memory-footprint gap is worth standardising. The ecosystem already has [...map.values()][n] . We didn't need Array.prototype.at either. We added it because negative indexing and cleaner syntax have value. Same principle here

"Via a linked list" would mean the cost of .drop(n).take(1) isn't avoidable, because it has to navigate the links :-)

You're right — both are O(n). The difference is memory, not time.
map.at(n) avoids allocating a temporary array of all entries, which matters for large Maps where spreading or toArray() causes memory spikes and GC pressure.
The iteration cost is identical; the allocation cost is not.

I think Jordan was suggesting that you use iterator helpers to do keys().drop(n).take(1) which won't cause memory spikes or GC pressure as keys() is an iterator and neither drop nor take will need to store more than one item at a time in memory.

Iterator helpers (drop , take ) are a great addition, but they're still Stage 3 and not yet universally available. More importantly, they require chaining multiple methods and calling .next().value to extract the result — which is less discoverable and ergonomic than a simple .at(n) .

Array.prototype.at already set the precedent: a simple, direct method for indexed access. Maps and Sets deserve the same. Iterator helpers are powerful, but they're a general-purpose tool; .at(n) is a specific, common operation that should be directly available.

function at(idx, iter) {
  let i = -1;
  for (let value of iter) {
    if (++i === idx) return value;
  }
}

let m = new Map([['foo', 'Foo'], ['bar', 'Bar']]);

at(1, m.keys()) // 'bar'

Edit: sorry for all the typos and edits to fix them! I accidentally hit enter before I was even half done, than paniced trying to fix it quickly

Stage 3 is when they’re safe to use, and they’re pretty universally shipped afaik - and polyfillable on every engine.

The difference is that arrays are indexed collections - not just ordered ones. Maps and Sets aren’t indexed, only ordered. Ordering doesn’t imply that an index makes sense.

You're right — a utility function works. The same could be said for Array.prototype.at (arr.at(n) vs function at(arr, n) { return arr[n]; } ). But we added it anyway because:

  1. Discoverability — New developers shouldn't need to find or write this utility
  2. Consistency — Arrays have .at() , Maps and Sets should too
  3. Chainabilitymap.at(n) works inline without nesting
  4. Performance — Native implementation can potentially optimize beyond userland loops
  5. Precedent — We add convenience methods all the time (e.g., Array.prototype.find vs manual loop)

*The question isn't 'can this be done?' — it's 'should this be a built-in for consistency and ergonomics?' I believe yes.

You're right — Maps and Sets aren't fundamentally indexed collections. But they are ordered, and developers frequently need to access entries by position (cache eviction, debugging, pagination).

The question isn't 'should Maps become Arrays?' — it's 'should ordered collections support indexed read-only access for convenience?'

We already have [...map.keys()][n] — a clunky, allocation-heavy version of this. .at(n) would be the same operation, but cleaner and without the memory spike.

This isn't changing the nature of Maps. It's just providing a direct path for an operation developers already perform.

developers frequently need to access entries by position (cache eviction, debugging, pagination).

This has not been my experience. For debugging, I have only ever seen the entire contents dumped out; for pagination, I've only ever seen an Array (what would be the point of using a set or map for that?). Can you explain a bit about the cache eviction use case?