Search code examples
javascriptnode-modulesdestructuring

How to do destructuring assignment to a higher scoped set of variables?


I'm trying to figure out how to do object destructuring assignment from within a code block, but where the receiving variables live at the module scope, not inside the block where the code runs without having to repeat the definitions in multiple scopes?

I was happily using object destructuring assignment like this at the top level scope of my module and it creates a bunch of top-level module-scoped variables which is what my code expects:

let {
    numToRead,
    numConcurrent,
    numWorkers,
    skipParsing,
    preOpenFiles,
    dir: sourceDir,
    binary: doBinary,
} = processArgs(spec);

This code executes when my module is run as a top level module and reads its arguments directly from the command line (processArgs() parses the command line). But, now I need to change the code so that this code runs conditionally (it's inside an if block) and sometimes the arguments are passed in via an options object in an exported function.

So, I started to write this:

// if executed directly from the command line, get options from the command line
if (require.main === module) {

    // module level configuration variables from command line arguments (with defaults)
    let {
        numToRead,
        numConcurrent,
        numWorkers,
        skipParsing,
        preOpenFiles,
        dir: sourceDir,
        binary: doBinary,
    } = processArgs(spec);

    // more code here
}

module.exports = function(options) {
    // run this from options passed in by another module, rather than the command line
    // this code also needs to populate the same top level module variables
}

But, this if block creates all these variables inside the block scope which is not what I need. I need them to be at the top level of the module.

Is there a way to get these variables to be automatically created at the top level of the module when the destructuring assignment is inside a block without having to repeat the definitions of all the top level variables?


The only way I was able to get it work has three separate lists of the named top level variables which seems like a bad practice that I would like to avoid. Can these be avoided and still end up with top level variables:

// top level module arguments
let numToRead = 0,
    numConcurrent = 4,
    numWorkers = 5,
    skipParsing = false,
    preOpenFiles = "false",
    sourceDir = "./buckets",
    doBinary = false;

function run(options) {
    // load arguments into top level module variables
    ({
        numToRead,
        numConcurrent,
        numWorkers,
        skipParsing,
        preOpenFiles,
        dir: sourceDir,
        binary: doBinary,
    } = options);

    analyzeWithWorkers().catch(err => {
        console.log(err);
        process.exit(1);
    });
}

// for running from the command line
if (require.main === module) {
    run(processArgs(spec));
}

// for calling this from another module
module.exports = function(options) {
    // have to fill in any missing arguments
    // here as all options are optional
    let fullOptions = Object.assign({
        numToRead,
        numConcurrent,
        numWorkers,
        skipParsing,
        preOpenFiles,
        dir: sourceDir,
        binary: doBinary}, options);
    run(fullOptions);
}

Solution

  • Since the other functions in the module depend on the option values, I think it would make the most sense to initialize those functions only once the option values have been defined in the main exported function. (This also has the benefit of avoiding reassignment as a side effect of the main function, which can make code harder to understand and test.) Going this route, you can put the argument values into the variables at once, while also renaming the properties, and also setting their default values, which I think is the key insight you need:

    function main(options) {
      const {
        numToRead = 0,
        numConcurrent = 4,
        numWorkers = 5,
        skipParsing = false,
        preOpenFiles = "false",
        dir: sourceDir = "./buckets",
        binary: doBinary = false
      } = options;
      function analyzeWithWorkers() {
        // reference variables here
        // ...
      }
      // initialize other functions if needed, referencing those variables
      analyzeWithWorkers().catch(err => {
          console.log(err);
          process.exit(1);
      });
    }
    if (require.main === module) {
      main(processArgs(spec));
    }
    module.exports = main;
    

    function main(options) {
      const {
        numToRead = 0,
        numConcurrent = 4,
        numWorkers = 5,
        skipParsing = false,
        preOpenFiles = "false",
        dir: sourceDir = "./buckets",
        binary: doBinary = false
      } = options;
      function analyzeWithWorkers() {
        console.log(skipParsing, sourceDir);
        return Promise.resolve();
      }
      // initialize other functions if needed, referencing those variables
      analyzeWithWorkers().catch(err => {
          console.log(err);
          process.exit(1);
      });
    }
    /*
    if (require.main === module) {
      main(processArgs(spec));
    }
    module.exports = main;
    */
    main({});
    main({ dir: 'someDir' });

    If the variables have to be defined on the top level, there's no way around repeating them all at least twice - once to declare on the top level, and once to possibly reassign inside the exported function. You can turn your 3-repetitions into 2-repetitions by assigning the default values inside run, the exported function, which assigns to the outside variables (assuming the module is always called externally by either the command line or by the main exported function).

    let numToRead,
        numConcurrent,
        numWorkers,
        skipParsing,
        preOpenFiles,
        sourceDir,
        doBinary;
    
    function run(options) {
        // load arguments into top level module variables
        ({
            numToRead = 0,
            numConcurrent = 4,
            numWorkers = 5,
            skipParsing = false,
            preOpenFiles = "false",
            dir: sourceDir = "./buckets",
            binary: doBinary = false
        } = options);
        analyzeWithWorkers().catch(err => {
            console.log(err);
            process.exit(1);
        });
    }
    
    function analyzeWithWorkers() {
        console.log(skipParsing, sourceDir);
        return Promise.resolve();
    }
    if (require.main === module) {
      run(processArgs(spec));
    }
    module.exports = run;
    

    let numToRead,
        numConcurrent,
        numWorkers,
        skipParsing,
        preOpenFiles,
        sourceDir,
        doBinary;
        
    function run(options) {
        // load arguments into top level module variables
        ({
            numToRead = 0,
            numConcurrent = 4,
            numWorkers = 5,
            skipParsing = false,
            preOpenFiles = "false",
            dir: sourceDir = "./buckets",
            binary: doBinary = false
        } = options);
        analyzeWithWorkers().catch(err => {
            console.log(err);
            process.exit(1);
        });
    }
    
    function analyzeWithWorkers() {
        console.log(skipParsing, sourceDir);
        return Promise.resolve();
    }
    run({});
    run({ dir: 'someDir' });