ES-Next dynamic import
Nathaniel

Nathaniel @natelindev

About: Full stack Typescript/Javascript Developer

Location:
SuZhou, CN
Joined:
Aug 30, 2019

ES-Next dynamic import

Publish Date: Aug 30 '19
7 2

Problem

Recently when writing a project of mine, I've written some javascript like this:

import itemA from '../items/itemA';
import itemB from '../items/itemB';
import itemC from '../items/itemC';
import itemD from '../items/itemD';
Enter fullscreen mode Exit fullscreen mode

I'd really like some syntax like this:

for(const id of ['A','B','C','D']) {
    import (item+id) from '../items/item' + id;
}
Enter fullscreen mode Exit fullscreen mode

Turned out there's this stage 4 proposal of ECMAScript called dynamic import that goes like:

(async () => {
  await import('./my-app.mjs');
})();
Enter fullscreen mode Exit fullscreen mode

and it's supported by all modern browsers as well as node.

Failed Attempt

Then I went on writing some line code like this:

importedItems = await Promise.all(
  itemNames.map(async itemName => {
    try {
      const imported = await import(`../items/${itemName}`);
      logger.debug(`Imported item: ${itemName}`);
      return imported;
    } catch (err) {
      logger.warning(`Failed to import item: ${itemName}`);
      return null;
    }
  })
);
Enter fullscreen mode Exit fullscreen mode

But for later use of importedItems, a TypeError has been raised stating that the importedItems are actually type of Module instead of expected item. I tried manual type casting like this:

return Item(imported);
Enter fullscreen mode Exit fullscreen mode

But it didn't work and since it's not yet standardized feature (Due to be standardized in ECMAScript 2020), there's little information about it so I have to figure it out myself.

After a long time with JSON.stringify() and console.log I finally figured it out.

Solution

It should work like this:

return imported.default;
Enter fullscreen mode Exit fullscreen mode

Full working snippet

importedItems = await Promise.all(
  itemNames.map(async itemName => {
    try {
      const imported = await import(`../items/${itemName}`);
      logger.debug(`Imported item: ${itemName}`);
      return imported.default;
    } catch (err) {
      logger.warning(`Failed to import item: ${itemName}`);
      return null;
    }
  })
);
Enter fullscreen mode Exit fullscreen mode

Reason

The reason is that when we use an import statement like this:

import item from './itemA';
Enter fullscreen mode Exit fullscreen mode

it automatically loads the default export of module 'itemA' into the item.

But when we do expression like dynamic import like this:

(async () => {
  const item = await import('./itemA');
})();
Enter fullscreen mode Exit fullscreen mode

the item is a Module , by accessing Module.default we are able to fetch its default export, same goes for any other exports.

originally posted on: https://blog.llldar.io/Article/View/44

Comments 2 total

  • Amin
    AminAug 30, 2019

    Let's say I have three modules.

    $ touch {main,fibonacci,luhn}.mjs
    
    // fibonacci.mjs
    function fibonacci(number, oldFibonacci = 1) {
      if (number <= 1) {
        return oldFibonacci
      }
    
      return fibonacci(number - 1, oldFibonacci * number)
    }
    
    export default fibonacci
    
    // luhn.mjs
    function luhn(number) {
      return Array.from(number.toString()).reduce(function(sum, digit, index) {
        if (index % 2 === 0) {
          return sum + digit
        }
    
        const doubled = digit * 2
    
        if (doubled > 9) {
          return sum + (doubled - 9)
        }
    
        return sum + doubled
      }, 0) % 10 === 0
    }
    
    export default luhn
    

    We could write an intermediary function that will return the default exported module out of a dynamic imported one.

    // main.mjs
    function importDefault(path) {
      return import(path).then(function(module) {
        return module.default
      })
    }
    
    Promise.all([
      importDefault("./fibonacci.mjs"),
      importDefault("./luhn.mjs")
    ]).then(function([ fibonacci, luhn ]) {
      console.log(fibonacci(5)) // 120
      console.log(luhn(732829320)) // true
      console.log(luhn(732829321)) // false
    })
    

    Of course, you can import named exported modules as well like usual.

    // main.mjs
    Promise.all([
      importDefault("./fibonacci.mjs"),
      importDefault("./luhn.mjs"),
      import("fs")
    ]).then(function([ fibonacci, luhn, { writeFile } ]) {
      console.log(fibonacci(5)) // 120
      console.log(luhn(732829320)) // true
      console.log(luhn(732829321)) // false
    
      writeFile("test.txt", "hello world!", function(error) {
        if (error) {
          console.error("test file error")
        } else {
          console.log("test file written")
        }
      })
    })
    

    N.B.: if you want to test this code, you'll have to run this command with a compliant version of Node.js supporting experimental modules.

    $ node --experimental-modules ./main.mjs
    
    • Nathaniel
      NathanielAug 30, 2019

      Nice complement, I didn't mention I was using esm. If anyone want to use the import export without having to name your file .mjs on node, try it out.

Add comment