ECMAScript proposal: Array.fromAsync()

[2022-11-27] dev, javascript, es proposal
(Ad, please don’t block)

This blog post is about the ECMAScript proposal “Array.fromAsync for JavaScript” by J. S. Choi. It introduces a static method for converting asynchronous iterables to Arrays.

Tools for working with synchronous iterables  

Currently JavaScript provides several tools for working with synchronous iterables – for example:

function* createSyncIterable() {
  yield 'a';
  yield 'b';
  yield 'c';
}

// Array-destructuring
const [elem0, elem1] = createSyncIterable();
assert.equal(elem0, 'a');
assert.equal(elem1, 'b');

// Spreading into Arrays
assert.deepEqual(
  ['>', ...createSyncIterable(), '<'],
  ['>', 'a', 'b', 'c', '<']
);

// Spreading into function arguments
const arr = [];
arr.push('>', ...createSyncIterable(), '<');
assert.deepEqual(
  arr,
  ['>', 'a', 'b', 'c', '<']
);

// Array.from()
assert.deepEqual(
  Array.from(createSyncIterable()),
  ['a', 'b', 'c']
);
assert.deepEqual(
  Array.from(createSyncIterable(), s => s + s),
  ['aa', 'bb', 'cc']
);

// for-of loop
for (const x of createSyncIterable()) {
  console.log(x);
}
// Output:
// a
// b
// c

For working with asynchronous iterables, we currently only have the for-await-of loop.

Array.fromAsync()  

Array.fromAsync() is the asynchronous version of Array.from():

interface ArrayConstructor {
  fromAsync<T>(
    asyncIterable: AsyncIterable<T>
  ): Promise<Array<T>>;
  fromAsync<T, U>(
    asyncIterable: AsyncIterable<T>,
    mapFn: (value: T, index: number) => U,
    thisArg?: any
  ): Promise<Array<U>>;
  // ···
}

Array.fromAsync() accepts up to three arguments:

  • asyncIterable is the asynchronous iterable that is converted to an Array.
  • The optional mapFn lets us transform the iterated values before they are added to the Array that is returned. If we provide this argument, Array.fromAsync() works similarly to the Array method .map().
  • The optional thisArg lets us specify the value of this for mapFn.

Given that the values in asyncIterable can’t be collected synchronously, Array.fromAsync() works asynchronously and returns a Promise for an Array. Therefore, we await its results in the following example:

async function* createAsyncIterable() {
  yield 1;
  yield 2;
  yield 3;
}

assert.deepEqual(
  await Array.fromAsync(createAsyncIterable()),
  [1, 2, 3]
);
assert.deepEqual(
  await Array.fromAsync(createAsyncIterable(), x => x * x),
  [1, 4, 9]
);

An implementation of Array.fromAsync()  

We could implement Array.fromAsync() like this:

async function arrayFromAsync(
  asyncIterable, mapFn=x=>x, thisArg=undefined
) {
  const result = [];
  for await (const elem of asyncIterable) {
    result.push(mapFn.call(thisArg, elem));
  }
  return result;
}

Example: reading chunks from a readable web stream  

In Node.js, readable web streams are asynchronously iterable. Therefore, we can use Array.fromAsync() to collect all the chunks (pieces of data) of a ReadableStream in an Array.

We’ll read the following file data.txt:

First line
Second line

This is the code:

import {Readable} from 'node:stream';
import * as fs from 'node:fs';
const readableStream = Readable.toWeb(
  fs.createReadStream('data.txt', 'utf-8')
);
const chunks = await Array.fromAsync(readableStream);
assert.deepEqual(
  chunks,
  ['First line\nSecond line']
);

Further reading