Is there a notation for dynamic indexes in array literals?

Viewed 76

When declaring object literals, it is possible to declare dynamic keys like this:

const foo = 'foo';

const obj = {
  [foo]: 'bar'
}

console.log(obj);

This syntax fails for arrays because the square bracket is interpreted as an array declaration, then the parser fails when reaching the colon. Example of use case : cloning an array while changing the last element:

const arr = [1, 2, 3];

const modifiedClone = [...arr, [arr.length - 1]: 4]; // Fails

Is there a syntax to declare dynamic indexes for array literals?

2 Answers

You could take Object.assign with an empty array as target and the given array and an object with an index property.

const arr = [1, 2, 3];

const modifiedClone = Object.assign([], arr, { [arr.length - 1]: 4 }); 

console.log(modifiedClone);

What you ask for is currently not possible using the array literal and spread syntax. This is because unlike object literals, an array is not defined by specific indexes but by the sequence that becomes its members.

At most, you can use elision to leave an index empty, e.g., [1, , 3] will produce 1, <empty>, 3 but you still have to be explicit about it, you cannot "skip over", this means that creating an array with only the first and tenth member becomes very hard [1,,,,,,,,,10]. Having more is definitely not recommended as an array literal.

With that said, the spread syntax ... with arrays takes values from an iterator, so [...arr] is similar to:

let result = [];
let it = arr[Symbol.iterator]();
let next = it.next();

while (!next.done)) {
    result.push(next.value);
    next = it.next();
}

With this knowledge, we can leverage generator functions and make some simple generic helper generators that transform an iterable and spread their results. For example

Get everything but the last item from an iterable:

/** 
 * 
 * @generator
 * @param {Iterable}
 * @yields {*} every value from the iterable but the last one
 */
function* initial(iterable) {
  const it = iterable[Symbol.iterator]();
  let last = it.next();
  let next = it.next();
  
  while(!next.done){
    yield last.value;
    last = next;
    next = it.next();
  }
}


const arr = [1, 2, 3];
const modifiedClone = [...initial(arr), 4];

console.log(modifiedClone);

Take the fist num values from an iterable:

/**
 *
 * @generator
 * @param {number} num - how many items to extract
 * @param {Iterable} itearable - iterable from where the results should be extracted
 * @yields {*} - at most `num` amount of results from `iterable`
 */
function* take(num, iterable) {
  const it = iterable[Symbol.iterator]();
  
  let {value, done} = it.next();
  for (let i = 0; i < num && !done; i++, {value, done} = it.next()) {
    yield value;
  }
}

const arr = [1, 2, 3];
const modifiedClone = [...take(arr.length - 1, arr), 4];

console.log(modifiedClone);

Go over an iterable and transform a value where needed:

/**
 * transform an iterable of key-value pairs by their key
 * 
 * @generator
 * @param {Map} replacements - new values for each of the keys
 * @param {Iterable} keyValueIterable - iterable which produces `[key, value]` pairs
 * @yields {*} - the values of the iterable transformed where needed
 */
function* replaceByKey(replacements, keyValueIterable) {
  const it = keyValueIterable[Symbol.iterator]();

  for (let {value: pair, done} = it.next(); !done; {value: pair, done} = it.next()) {
    let [key, value] = pair;
    if (replacements.has(key))
      yield replacements.get(key);
    else
      yield value;
  }
}

const arr = ["a", "b", "c", "d", "e"];

const replace = new Map()
  .set(1, "foo")
  .set(arr.length-1, "bar");
const modifiedClone = [...replaceByKey(replace, arr.entries())];

console.log(modifiedClone);

More possibilities available depending on what is need.

Related