1
\$\begingroup\$

I want to implement a function that enumerates all files in directory. I think the function should

  • Return a Promise, because underlying readdir call is prefered over readdirSync.
  • Be a generator function, because checking statSync(...).isFile() might take some time and maybe, I will need only first few files...

So my attempt

import * as fs from 'fs';
import * as path from 'path';

function enumerateFiles(dir: string): Promise<IterableIterator<string>> {
    let result = function* (files: string[]): IterableIterator<string> {
        for (let enty of files) {
            let fullpath = path.join(dir, enty);

            if (fs.statSync(fullpath).isFile())
                yield fullpath;
        }
    };

    return new Promise<IterableIterator<string>>((resolve, reject) => {
        fs.readdir(dir, (err, files) => {
            if (err)
                reject(err);
            else
                resolve(result(files));
        });
    });
}

And usage

for (let file of await enumerateFiles(String.raw`C:\Windows\Web\Screen`))
    console.log(file);

// C:\Windows\Web\Screen\img100.jpg
// C:\Windows\Web\Screen\img101.jpg
// ...

What do you think? I'm really not sure about combining Promises with generator functions. Especially in this order...

\$\endgroup\$
1
\$\begingroup\$

Right now the biggest problem with this function that I see is the mixing of async and sync fs APIs. This is confusing to the consumer - if I have to deal with promises, then chances are I will expect you are doing everything asynchronously behind the scenes.

I think this function is a good idea, but it would be better if you took advantage of the Async iterator proposal, which Typescript can transpile.

import * as fs from 'fs'
import { join } from 'path'
import { promisify } from 'util'

const statPromise = promisify(fs.stat)
const readDirPromise = promisify(fs.readdir)

const isFile = (path: string) => statPromise(path)
  .then(stats => stats.isFile())

export async function * enumerateFiles (path: string): AsyncIterableIterator<string> {
  const files = await readDirPromise(path)
  for (const file of files) {
    if (await isFile(join(path, file)) {
      yield file
    }
  }
}

And here's an example of usage:

// Async function is required.
async function main () {
  for await (const file of enumerateFiles('.')) {
    console.log(file)
  }
}

main()

Note that to use async iterators, you need to add esnext.asynciterable to the lib section of your tsconfig.json.

\$\endgroup\$
  • \$\begingroup\$ Sorry for the delay in posting this - I was pretty sure I knew the async iterator proposal was the way to do this more cleanly, but didn't have the time to look into it until today. \$\endgroup\$ – Gerrit0 Feb 14 '18 at 23:43
  • \$\begingroup\$ Hey, this is really cool solution! Thanks for both, notifying me about this proposal and demonstrating it in my problem. I’m starting using this pattern from now :-). Again, thanks a lot! \$\endgroup\$ – Matěj Pokorný Feb 15 '18 at 23:20
1
\$\begingroup\$
for (let file of await enumerateFiles(String.raw`C:\Windows\Web\Screen`))

The await looks like a leaky abstraction. Could your generator naturally behave synchronously? A quote from Exploring ES6

22.1.5 Use case: receiving asynchronous data

Generators can receive input from next() via yield. That means that you can wake up a generator whenever new data arrives asynchronously and to the generator it feels like it receives the data synchronously.


\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.