Provide an easy way to create a new array, filled via a mapping function

It's pretty common to want to create an array of a given length, and populate it depending on its index. At a really basic level, something like [1, 2, 3, 4, 5].

JS has a lot of nice high-level FP-style array methods, but creating something like [1, 2, 3, 4, 5] is surprisingly clunky, or exposes you to some of the less idiomatic parts of JS.

Eg:

const result = Array(5).map((_, i) => i + 1);

The above doesn't work due to how methods treat 'empty' array entries. A common workaround is:

const result = Array(5).fill(0).map((_, i) => i + 1);

This still exposes you to the weirdness of 'empty' entries, iterates over the array twice, and leaves the reader with an irrelevant fill value to ignore.

const result = [...Array(5)].map((_, i) => i + 1);

This avoids the redundant fill value, but still exposes you to the weirdness of 'empty' entries and an extra iteration.

Alternatively:

const result = Array.from({ length: 5 }, (_, i) => i + 1);

The above avoids the redundant fill value, and the additional iteration, but it feels like a 'trick'. You need to create a value that will cause Array.from to create an array of a given length, and treat each entry as non-empty.

This stuff is easy with a plain for-loop of course:

const result = [];
for (let i = 0; i < 5; i++) result[i] = i + 1;

…but that doesn't fit in with the high-level FP-style methods that already exist.

Should we have something to make this easier, such as:

const result = Array.create(5, (i) => i + 1);

Although all the examples in this post could be satisfied with some sort of range, I don't think the functionality should be limited to ranges. It should be useful in other cases, eg:

function createPoints(input) {
  // input is an array of numbers like [x, y, x, y, x, y]
  return Array.create(input.length / 2, (i) => ({
    x: input[i * 2],
    y: input[i * 2 + 1],
  }));
}
5 Likes

GitHub - tc39/proposal-Number.range: A proposal for ECMAScript to add a built-in Number.range() is probably what you're looking for.

1 Like

That's my fault for making the examples overly simple. The times I've needed something like this haven't been limited to ranges. I've added an additional example to clarify.

You could still do Number.range(0, count).map(i => ...).toArray() when combined with GitHub - tc39/proposal-iterator-helpers: Methods for working with iterators in ECMAScript. Is that sufficient?

1 Like

That seems significantly more complicated than the workarounds currently available. It has a similar problem as fill(0) in that it introduces a step that feels redundant. But, on the plus side it doesn't introduce an additional iteration.

function createPoints(input) {
  return Number.range(0, input.length / 2)
    .map((i) => ({
      x: input[i * 2],
      y: input[i * 2 + 1],
    }))
    .toArray();
}

// vs

function createPoints(input) {
  return Array.from({ length: input.length / 2 }, (_, i) => ({
    x: input[i * 2],
    y: input[i * 2 + 1],
  }));
}

Fwiw, lodash has _.times, so there's prior art here.

function createPoints(input) {
  return _.times(input.length / 2, (i) => ({
    x: input[i * 2],
    y: input[i * 2 + 1],
  }));
}

A past proposal is http://array.build, but the current best practice is indeed the Array.from pattern you provided (never use Array() or sparse arrays, please, ever)

The number range proposal is another good alternative.

2 Likes

Any idea why the Array.build proposal hasn't progressed?

Array.from({ length }, mappingFn) is the technique I currently use, but having to create a fake array makes it feel like a hack.

2 Likes

Nobody's brought it forward since the initial pre-ES6 suggestion, afaict.

At this point tho it'd probably need a really persuasive argument showing that removing the { length } is worth the API addition to the language.

1 Like

I guess we're stuck with the hacky way then :disappointed:

It's not about the character count of it though, it's about how we intuitively understand it. I agree that using Array.from() with a { length } object requires some unnecessary advance knowledge about the concept of array-like, and it just feels like a hack. I've also often had a need to create an empty array many times, and I'm always hesitant about using Array.from(). I'll use it in my own code, since I know what it means, but I usually just use a for loop instead in the code I write at my work, so that I don't have to use such an unnecessarily advanced trick to do something so simple.

However, I would be perfectly happy with the potential future solution @claudiameadows demonstrated, of using the iterator helpers proposal with a range.

To be clear, I would love to see something like Array.build added to the language; I just don't think I could make a persuasive argument for it to the committee.

2 Likes

I think it's worth a shot! New APIs don't have nearly as high a bar as new syntax.

Duplicate thread to tie together Array.create

Similar to _.times

Alright, well perhaps let try brainstorming a couple of options here to see what we like best.

Some of these solutions let you optionally provide a mapping function. Other ones require you to use .map() afterwards.

Array.create(length, mappingFn?) // Original idea
Array.fromLength(length, mappingFn?) // Also works
Array.build(length, mapFn?) // From the Array.build proposal

// Fills with undefined, and requires people to use .map() afterwards.
// Does not take a mappingFn
// Can be thought of as a direct replacement for new Array(length)
Array.fromLength(length)

Array.prototype.repeat(length) // e.g. [2].repeat(3) == [2, 2, 2] -- paralells with string.repeat(). Similar to array multiplication in python.
Array.prototype.repeat(length, mapFn?)

// -- Functions that pre-fill the array with other stuff -- //

Array.fromRange(start?, end) // e.g. Array.fromRange(2, 5) == [2, 3, 4]
Array.filled(length, value?) // e.g. Array.filled(2, null) == [null, null], value defaults to undefined

These are just the solutions I could come up with (or that others have come up with already). Usability-wise, solutions that don't include a mapping function should work just as well, because you can just use .map() afterwards, however, I presume it will be less optimized as engines will have to iterate a second time over the values as they create the array.

fromLength with a mapping function seems simplest to me. "repeat" sounds like a horrifyingly quick way to generate a memory error :-p

3 Likes

I do also like how .fromLength() gives a little more context about what the parameters do (at least, what the first parameter would do). Array.build() and Array.create() doesn't tell you anything about what its parameters do.

2 Likes

Array.withLength ?

EDIT: I already don’t like this :stuck_out_tongue_closed_eyes:

I guess it would be superfluous once the Number.range proposal got through. It would offer not just

const result = Number.range(0, count).map(i => i+1).toArray();

but also

const result = Array.from(Number.range(0, count).map(i => i+1));
const result = Array.from(Number.range(0, count), i => i+1);

where I don't see much advantage of combining from and range into a single fromRange method.