Search code examples
javascriptarraysecmascript-6es6-promise

Filtering an array with a function that returns a promise


Given

let arr = [1,2,3];

function filter(num) {
  return new Promise((res, rej) => {
    setTimeout(() => {
      if( num === 3 ) {
        res(num);
      } else {
        rej();
      }
    }, 1);
  });
 }

 function filterNums() {
   return Promise.all(arr.filter(filter));
 }

 filterNums().then(results => {
   let l = results.length;
   // length should be 1, but is 3
 });

The length is 3 because Promises are returned, not values. Is there a way to filter the array with a function that returns a Promise?

Note: For this example, fs.stat has been replaced with setTimeout, see https://github.com/silenceisgolden/learn-esnext/blob/array-filter-async-function/tutorials/array-filter-with-async-function.js for the specific code.


Solution

  • As mentioned in the comments, Array.prototype.filter is synchronous and therefore does not support Promises.

    Since you can now (theoretically) subclass built-in types with ES6, you should be able to add your own asynchronous method which wraps the existing filter function:

    Note: I've commented out the subclassing, because it's not supported by Babel just yet for Arrays

    class AsyncArray /*extends Array*/ {
      constructor(arr) {
        this.data = arr; // In place of Array subclassing
      }
    
      filterAsync(predicate) {
         // Take a copy of the array, it might mutate by the time we've finished
        const data = Array.from(this.data);
        // Transform all the elements into an array of promises using the predicate
        // as the promise
        return Promise.all(data.map((element, index) => predicate(element, index, data)))
        // Use the result of the promises to call the underlying sync filter function
          .then(result => {
            return data.filter((element, index) => {
              return result[index];
            });
          });
      }
    }
    // Create an instance of your subclass instead
    let arr = new AsyncArray([1,2,3,4,5]);
    // Pass in your own predicate
    arr.filterAsync(async (element) => {
      return new Promise(res => {
        setTimeout(() => {
          res(element > 3);
        }, 1);
      });
    }).then(result => {
      console.log(result)
    });
    

    Babel REPL Demo