You want arr[-1] to return the last element by wrapping a JavaScript array in a Proxy. What must the get handler do with the property key it receives, and what must it still forward?
answer
- keys arrive as strings, never numbers
- guard symbols before coercing
- translate to length plus n
- forward the rest with Reflect.get
- at(-1) already does this
basics
~20 sProperty keys reach a get trap as strings or symbols, never numbers, so the handler must convert the key to a number, verify it is a negative integer, and translate it to length plus that index. Everything else must be forwarded unchanged.
solid answer
~40 sTwo things. First, the key: `arr[-1]` is an ordinary property read of the string `"-1"`, so the trap receives `"-1"`, not the number `-1`. The handler has to convert with `Number(key)`, check `Number.isInteger` and that it is negative, then read `target[target.length + n]`. Second, everything else has to keep working — positive indices, `length`, every array method, and `Symbol.iterator`. The clean way is a single fallback of `Reflect.get(target, key, receiver)` rather than hand-listing cases, because a missed key silently breaks iteration or `map`. If you also want `arr[-1] = x` to write the last slot you need the same translation in a `set` trap. Worth saying out loud: since ES2022 `arr.at(-1)` does this natively, so the proxy is only interesting when you must keep bracket syntax.
code
javascript · 17 linesconst target = [10, 20, 30];
const p = new Proxy(target, {
get(t, key, receiver) {
if (typeof key === 'string') {
const n = Number(key);
if (Number.isInteger(n) && n < 0) return t[t.length + n];
}
return Reflect.get(t, key, receiver);
}
});
console.log(p[-1]); // 30
console.log(p[0]); // 10
console.log(p.length); // 3
console.log([...p]); // [10, 20, 30]
console.log(target.at(-1)); // 30 — ES2022, no proxy neededgo deeper
Know that bracket access converts whatever is inside to a string key, so arr[-1] reads a property literally named "-1" and is undefined on a normal array.
Be able to write the handler: convert the string key, verify it is a negative integer, translate against length, and forward every other key untouched so iteration and methods keep working.
Show that you weigh the wrapper against at(-1): a proxy adds a per-access trap call and a second object identity, and you would only accept that where bracket syntax at foreign call sites is non-negotiable.
Own the boundary question — if such a wrapper exists, decide where a proxied array may travel, since it is not clone-transferable and compares unequal to its target across module, worker and cache boundaries.
## Why the key is a string In JavaScript, ordinary property keys are strings or symbols — there is no integer key type. Array indices are simply string keys that happen to look like canonical numbers, and the bracket form converts whatever you put inside to a property key first. So `arr[-1]` reads the property named `"-1"`, and `arr[0]` reads `"0"`. On a real array `"-1"` is not an index at all; it is just an ordinary property that nobody set, which is why plain `arr[-1]` is `undefined` rather than the last element. A `get` trap therefore receives `"-1"` as its `key` argument. Comparing `key < 0` happens to work through coercion, but `typeof key === 'number'` never does, and `key === -1` is always false. Convert explicitly. ## A correct implementation ```js function withNegativeIndices(array) { return new Proxy(array, { get(target, key, receiver) { if (typeof key === 'string') { const n = Number(key); if (Number.isInteger(n) && n < 0) { return target[target.length + n]; } } return Reflect.get(target, key, receiver); }, set(target, key, value, receiver) { if (typeof key === 'string') { const n = Number(key); if (Number.isInteger(n) && n < 0) { target[target.length + n] = value; return true; } } return Reflect.set(target, key, value, receiver); } }); } const xs = withNegativeIndices(['a', 'b', 'c']); xs[-1]; // 'c' xs[0]; // 'a' xs.length; // 3 [...xs]; // ['a','b','c'] ``` Four details matter here. **Guard on `typeof key === 'string'` first.** Symbols must never reach `Number()`, and `Symbol.iterator` is read on every spread and `for...of`. Coercing a symbol throws a `TypeError`, so a trap that skips the guard breaks iteration in a way that looks unrelated to the code you wrote. **Use `Number.isInteger`, not `parseInt`.** `parseInt('-1abc')` is `-1`, which would map a nonsense key onto a real element. `Number('-1abc')` is `NaN` and `Number.isInteger` rejects it, which is the behaviour you want. **Forward everything else through `Reflect.get(target, key, receiver)`.** Hand-writing `return target[key]` mostly works for arrays, but forwarding via `Reflect` keeps the `receiver` argument correct for accessor properties, and it is the habit that keeps you out of trouble on non-array targets. **Decide what out-of-range means.** `xs[-10]` on a three-element array computes index `-7` and yields `undefined`. That is usually the right answer — it matches `at(-10)` — but if you would rather throw, this is the place. ## What this does not fix The proxy only affects bracket access on the proxy itself. `xs.slice(-1)` already worked, because `slice` interprets negative arguments itself. Methods that read indices internally do so on the target through their own `this`, so they never see negative keys either. And the returned proxy is not the array: `Array.isArray(xs)` is `true` (that check deliberately sees through proxies), but `xs !== array`, so a `Map` keyed by the original array will not find the proxy, and passing it to `structuredClone` or `postMessage` fails. ## The version-aware answer Since ES2022, `Array.prototype.at` does exactly this job: `array.at(-1)` is the last element, with no wrapper, no allocation and no per-access trap cost. `String.prototype.at` and typed arrays have it too. In an interview, implementing the proxy demonstrates that you understand property keys and traps; recommending `at()` demonstrates that you know when not to reach for a proxy. Say both. The proxy is only genuinely warranted when the bracket syntax itself is a requirement — for example when the value is handed to third-party code that indexes it directly and you cannot change the call sites.
- What breaks if the trap calls Number(key) without first checking that the key is a string?Symbol keys throw. `Symbol.iterator` is read on every spread, `for...of` and destructuring, and `Number(someSymbol)` raises a `TypeError`, so iteration over the proxy dies with an error that points at your handler rather than at the call site. Guard with `typeof key === 'string'` before any numeric conversion.
- Would you ship this proxy in production code today?Rarely. `Array.prototype.at(-1)` has done the job natively since ES2022 with no wrapper and no per-access cost, and the proxy adds an object that is not the array — different identity, not structured-cloneable, slower on every read. The proxy is justified only when bracket syntax is a hard requirement at call sites you cannot change.
- Why does Array.isArray still return true for the proxy?That check is specified to look through proxy layers at the target's kind rather than at the object in hand, so a proxy whose target is an array reports `true`. It is a deliberate exception: most identity-flavoured checks — `===`, `Map`/`WeakMap` key lookup, structured cloning — see the proxy as a distinct object.
saying these in an interview costs you the question
- Expects the trap key to be a number
- Compares key === -1 inside the handler
- Coerces symbol keys and breaks iteration
- Uses parseInt, so "-1abc" maps to an element
- Forgets to forward length and array methods