3

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?

Guerric P
  • 30,447
  • 6
  • 48
  • 86
  • 2
    With an array literal, you can only declare the array with its members. You cannot really do the same as the object spread because fundamentally array syntax doesn't work like that - you don't define members by their position, you just define the sequence that gets turned into the array. – VLAZ Dec 04 '20 at 09:16
  • 1
    Just write `const modifiedClone = [...arr]; modifiedClone[arr.length - 1] = 4;`? – Bergi Dec 04 '20 at 09:30
  • @Bergi yes obviously, but my question is about a single instruction to achieve this – Guerric P Dec 04 '20 at 09:31
  • 1
    You can put everything into an IIFE if you want to make it a single expression :-) – Bergi Dec 04 '20 at 09:41
  • I prefer Nina Scholz's answer though! – Guerric P Dec 04 '20 at 09:46
  • 1
    There is a proposal [which would allow you to skip and rewind iterators](https://github.com/tc39/proposal-deiter). With that it would be possible to be more flexible and/or with [iterator helpers](https://github.com/tc39/proposal-iterator-helpers). If you want to both spread *and* overwrite, you can leverage generators for that. Whether it's worth it depends but it's an option. – VLAZ Dec 04 '20 at 10:06

2 Answers2

2

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);
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
0

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.

VLAZ
  • 26,331
  • 9
  • 49
  • 67