This article's content
Module types in JavaScript and TypeScript

CommonJS modules (exports and require)

Mainly used in Node.js:

  • Node.js heavily relies on CommonJS modules for organizing code. In Node.js, each file is treated as a module, and modules can be loaded into other modules using the require function.

Synchronous Loading

CommonJS modules are designed to be loaded synchronously. This means that the code within a module is executed immediately upon loading, and further code execution is blocked until loading is done. This synchronous nature is suitable for server-side applications, where file I/O operations are typically fast.

Single Instance:

  • CommonJS modules are cached after the first time they are loaded. Subsequent require calls for the same module return the cached instance. This helps in avoiding unnecessary re-execution of the module's code.
const privateVariable = "I am private";

function privateFunction() {
  console.log("This is a private function");
}

// Exporting public functionality
module.exports = {
  publicVariable: "I am public",
  publicFunction: function() {
    console.log("This is a public function");
  }
};

can be used like that:

const myModule = require('./myModule');

console.log(myModule.publicVariable);
myModule.publicFunction();

Syntax in TypeScript

// Export
export = someFunction;

// Import
import someFunction = require('./someModule');

CommonJS modules played a crucial role in the early development of server-side JavaScript, especially with the rise of Node.js. However, with the advent of ECMAScript 6 (ES6) modules, which offer a more standardized and versatile module system, the usage of CommonJS has somewhat diminished in modern front-end development.

Can CommonJS be used in the browser?

Yes, but the trend in frontend development is towards using native ES6 modules and more modern tools like Webpack with Babel to transpile code, rather than relying on CommonJS directly. That being said, while CommonJS modules were originally designed for server-side JavaScript (Node.js), they can also be used on the frontend, especially in scenarios where bundlers or transpilers are employed to make them compatible with browsers. However, there are some considerations to keep in mind:

  1. Compatibility:
    • Browsers do not natively support CommonJS modules. If you want to use CommonJS modules in the frontend, you need a tool like Browserify or Webpack, which can bundle your CommonJS-style code into a format that browsers can understand.
  2. Synchronous Loading:
    • CommonJS modules are designed for synchronous loading, which may not be suitable for all frontend scenarios. Browsers typically prefer asynchronous loading to avoid blocking the rendering of the page. As a result, bundlers often transform CommonJS code to work asynchronously on the frontend.
  3. Bundle Size:
    • CommonJS modules, when used directly in the frontend, might contribute to larger bundle sizes. This is because the bundler needs to include not only the code but also the CommonJS runtime, which can add overhead.
  4. ES6 Modules:
    • With the widespread adoption of ECMAScript 6 (ES6) modules, many frontend developers prefer using the native module system supported by browsers. ES6 modules offer a more standardized and flexible module syntax that is natively supported by modern browsers.

ES6 modules

ESM, or ECMAScript Modules, is the official module system introduced in ECMAScript 6 (ES6), also known as ECMAScript 2015. ESM provides a native, standardized way to organize and structure code in JavaScript applications. Unlike CommonJS, which is synchronous and primarily used in Node.js, ESM is designed to work in both browser and server-side environments. Here are some key features and aspects of ECMAScript Modules (ESM):

  1. Asynchronous Loading:
    • One of the significant differences from CommonJS is that ESM is designed for asynchronous loading. This makes it suitable for modern, performance-oriented web development, where asynchronous loading is preferred to avoid blocking the main thread.
  2. Browser and Node.js Support:
    • ESM is supported natively in modern browsers and in recent versions of Node.js. This native support simplifies the development process, eliminating the need for transpilers or bundlers in many cases.
  3. Static Analysis:
    • ESM supports static analysis, meaning that dependencies are determined at compile time rather than runtime. This allows tools to optimize the loading of modules.
  4. Encapsulation
    • Everything inside an ES6 module is private by default, and runs in strict mode (there’s no need for 'use strict').
  5. Circular Dependencies:
    • ESM handles circular dependencies more effectively than CommonJS. Circular dependencies occur when two or more modules depend on each other.

Syntax for JavaScript and TypeScript is the same:

// Exporting a variable or function
export const myVariable = "Hello, ESM!";

// Exporting a class
export class MyClass { /*...*/ }

// Exporting a function
export function myFunction() { /*...*/ }

Importing

// Importing a named export
import { myVariable, myFunction } from './myModule';

// Importing a default export
import MyDefaultExport from './myModule';

More syntax variations of import.

import defaultExport from "module-name";
import * as name from "module-name";
import { export1 } from "module-name";
import { export1 as alias1 } from "module-name";
import { default as alias } from "module-name";
import { export1, export2 } from "module-name";
import { export1, export2 as alias2, /* … */ } from "module-name";
import { "string name" as alias } from "module-name";
import defaultExport, { export1, /* … */ } from "module-name";
import defaultExport, * as name from "module-name";
import "module-name";

Dynamic Imports:

  • ESM introduces the import() function for dynamic imports. This allows you to load modules asynchronously, improving performance by loading only the necessary modules when they are needed.
// Dynamic import
import('./myModule').then((module) => {
  // Use the module
});

ESM is considered the modern standard for JavaScript modules, and its native support in both browsers and Node.js makes it a versatile and powerful choice for organizing and structuring code in contemporary JavaScript applications.

In order to use the import declaration in a source file, the file must be interpreted by the runtime as a module.

When you are using ECMAScript Modules (ESM) in a browser environment, you need to specify the type="module" attribute in the script tag to indicate that the script should be treated as an ECMAScript module. This is necessary because the default script type is assumed to be JavaScript (non-module).

<!-- Using ESM in the browser -->
<script type="module" src="main.js"></script>

When you are using ECMAScript Modules (ESM) in a browser environment, the server should serve the module files with the MIME type application/javascript. This ensures that the browser correctly interprets the file as JavaScript code.

Here's an example of how the server might set the MIME type for JavaScript files:

Content-Type: application/javascript

There is a difference in how regular <script> tags and ECMAScript Modules (ESM) handle cross-origin requests and adhere to the Same-Origin Policy.

Regular <script> Tags:

  • Traditional <script> tags do not enforce the Same-Origin Policy. They can fetch scripts from other domains without restrictions. This behavior is known as script-src cross-origin loading.
<!-- Regular script tag fetching a script from another domain --> <script src="https://example.com/script.js"></script>
  • This lack of restriction for regular <script> tags can pose security risks, as it allows scripts from external domains to be executed on a web page without proper control.

ECMAScript Modules (ESM):

  • ECMAScript Modules, on the other hand, are subject to the Same-Origin Policy. When you use the import statement to fetch a module, the browser enforces CORS (Cross-Origin Resource Sharing) policies.
  • If the module is on a different domain, the server hosting the module needs to include the appropriate CORS headers to allow the cross-origin request. Without proper CORS headers, the browser will block the request.
<!-- Module script tag fetching a module from another domain -->
<script type="module" src="https://example.com/module.js"></script>

This requires CORS to be set correctly on the server. The following example CORS header allows any origin to access the resource:

Access-Control-Allow-Origin: *

Modules won’t send cookies or other header credentials unless a crossorigin="use-credentials" attribute is added to the <script> tag and the response contains the header Access-Control-Allow-Credentials: true.

ESM in Node.js

In Node.js, you can use ECMAScript Modules (ESM) by following one of the methods:

You can specify that your project is using ESM by adding a "type" field with the value "module" to your package.json file. With this configuration, all .js files in your project are treated as ESM.

// package.json
{
  "type": "module",
  "main": "main.js"
}

Alternatively, you can use the .mjs file extension for your ESM files. Files with this extension are treated as ESM by default.

// main.mjs
import { myFunction } from './myModule';

myFunction();

When running a script with Node.js, you can explicitly specify the input type as "module" using the --input-type flag. This is useful if you want to run a specific script as an ESM module without modifying the package.json or file extensions.

node --input-type=module myScript.js

The "type": "module" approach in package.json is commonly used as it provides a project-wide setting, while the .mjs extension is useful for individual files. The --input-type flag allows for on-the-fly specification when running scripts.

Dynamic module imports with import()

Dynamic import(), which does not require scripts of type="module".

The following error indicates that you are attempting to use the require() statement to import an ECMAScript Module (ESM) in a context where CommonJS is expected. This error typically occurs when there is a mix of module types in your Node.js project, so if you are using ESM in your project (e.g., by setting "type": "module" in package.json or using .mjs file extension), you should consistently use import and export syntax throughout your codebase.

(node:26444) [ERR_REQUIRE_ESM] Error Plugin: xyz [ERR_REQUIRE_ESM]: require() of ES Module C:\code\xyz\node_modules\somepackage\lib\somepackage.js from C:\code\xyz\dist\index.js not supported.
Instead change the require of somepackage.js in C:\code\xyz\dist\index.js to a dynamic import() which is available in all CommonJS modules.

Here's an example of how this error might occur:

// somepackage.js - ESM module
export const someFunction = () => {
  console.log('This is an ESM module.');
};

Next, we make the mistake of using require to import ESM module in CommonJS module:

// index.js - CommonJS module
const somePackage = require('somepackage'); // This line would trigger the error

somePackage.someFunction();

To fix this either ensure that your entire codebase uses the same module type: Either convert your project to CommonJS by removing "type": "module" from package.json (or renaming .mjs files to .js), or convert the entire project to ESM by ensuring that all files use import and export syntax.

If you want to keep the mixed module types, you can use dynamic import to load the ESM module:

// index.js - CommonJS module
const somePackage = await import('somepackage');

somePackage.someFunction();

Error: Cannot use import statement outside a module

Cannot use import statement outside a module

This error mainly occurs when you use the import keyword to import a module in a file that Node.js does not recognize as an ECMAScript module. Or when you omit the type="module" attribute in a script tag.

  • If your project is intended to use ESM but lacks the "type": "module" field in package.json, Node.js will treat files with .js extensions as CommonJS by default.
  • To use ESM, either change the extension to .mjs or set "type": "module".

CommonJS vs. ECMAScript modules

CommonJS and ES6 modules cannot be mixed together, because apart from the different syntax for importing and exporting, CommonJS and ES6 modules execute code at different times when code is being imported: ES6 modules are pre-parsed in order to resolve further imports before code is executed. CommonJS modules load dependencies on demand while executing the code.

// CommonJS modules

// ---------------------------------
// one.js
console.log('running one.js');
const hello = require('./two.js');
console.log(hello);

// ---------------------------------
// two.js
console.log('running two.js');
module.exports = 'Hello from two.js';
running one.js
running two.js
hello from two.js

But with ES2015

// ES2015 modules

// ---------------------------------
// one.js
console.log('running one.js');
import { hello } from './two.js';
console.log(hello);

// ---------------------------------
// two.js
console.log('running two.js');
export const hello = 'Hello from two.js';
running two.js
running one.js
hello from two.js

ESM is natively supported by Node.js 12 and later.

Other module types

There used to be other module types such as Asynchronous Module Definition (AMD) and Universal Module Definition (UMD) which do not play a big role anymore in modern JavaScript developing.

AMD

The idea with AMD was to bring require known from NodeJS to the browser but loading the modules asynchronously. The following example only works if you have requirejs on your website.

// Define
define(['dependency'], function (dependency) {
  return function() { /*...*/ };
});

// Require
require(['module'], function(module) {
  // Use module
});

It's less common to use AMD with TypeScript directly. Most often, TypeScript is used with CommonJS or ES6 Modules.

UMD

Basically, a UMD module is a JavaScript file that tries to guess at runtime which module system it’s being used in, and then it acts as that kind of module. So you can load the file in a plain <script>, or you can load it from an AMD module loader, or you can load it as a Node.js module, and it will always do something sensible.

(function (root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD
    define(['dependency'], factory);
  } else if (typeof module === 'object' && module.exports) {
    // CommonJS
    module.exports = factory(require('dependency'));
  } else {
    // Browser global
    root.returnExports = factory(root.dependency);
  }
}(typeof self !== 'undefined' ? self : this, function (dependency) {
  // Module definition
}));

Setting up TypeScript with ESM for browser and Node.js

To set up a TypeScript project to use ECMAScript Modules (ESM), you need to configure TypeScript to generate code compatible with ESM and also ensure that Node.js recognizes your project as an ESM project. Here are the steps to set up a TypeScript project with ESM:

npm install -g typescript
npm install --save-dev typescript ts-node
// tsconfig.json
{
  "compilerOptions": {
    "module": "ESNext",
    "target": "ESNext",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "strict": true
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules"]
}
  • "module": "ESNext": Specifies that TypeScript should generate ESM code.
  • "target": "ESNext": Specifies the ECMAScript version that TypeScript will generate.
  • "moduleResolution": "node": Enables Node.js module resolution.

Create a simple TypeScript file in a src directory:

// src/main.ts
export const greet = (name: string): string => {
  return `Hello, ${name}!`;
};

console.log(greet('World'));

Run TypeScript Code using ts-node with npm start:

// package.json
{
  "scripts": {
    "start": "ts-node src/main.ts"
  }
}

If you are using Node.js directly to run your code, ensure to use:

node --experimental-specifier-resolution=node dist/main.js

TypeScript compilerOptions and modules

compilerOption esModuleInterop explained

"esModuleInterop" : "true": addresses situations where you are using both ESM and CommonJS modules in the same project and need to seamlessly import and use modules from both systems. If set to true, you can use import directly, and TypeScript will handle the necessary adjustments:

// ESM with esModuleInterop enabled
import myModule from './myModule';

If set to false, you may need to use the more verbose syntax for importing CommonJS modules with default exports

// ESM without esModuleInterop
import * as myModule from './myModule';
const defaultValue = myModule.default;

esModuleInterop is especially useful when you are working with existing code or third-party libraries that are written in CommonJS but you want to use them in a project that primarily uses ESM.

compilerOption module explained

module specifies what JS code tsc will emit:

If the input source code is:

import { createSourceFile } from "typescript"

then module: "commonjs" will emit

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const typescript_1 = require("typescript");

and module: "esnext" will emit

import { createSourceFile } from "typescript"

You cannot write imports in the style of import fs = require("fs") under --module es2015 (or higher ES targets) because there is no require to speak of in the ES module system. 

Additionally, using top-level await is only allowed in --module es2022 (or higher) or system because it requires corresponding support in the module loading system.

Specifying module: "nodenext" implies that moduleResolution is set to nodenext (a new resolution mode designed for Node’s specific implementation of co-existing ESM and CJS).

compilerOption moduleResolution explained

This defines the algorithm that answers the question where and how to look for a module. Looking in a folder "node_modules" is related to Node but also many other non-Node code has adopted this location for convenience, but is is not going to be correct for every runtime.

Using nodenext requires that your file endings are either mts or mjs for ESM and cts or cjs for CommonJS.

Setting up TypeScript with CommonJS for Node.js only

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "CommonJS",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules"]
}
  • "target": "ES2020": Specifies the ECMAScript version to target.
  • "module": "CommonJS": Specifies the module system to use (CommonJS is the default for Node.js).

Create a simple TypeScript file in the src directory:

// src/index.ts
const greeting: string = 'Hello, Node.js!';

console.log(greeting);

If you want to have type information for Node.js in your TypeScript code, you can install the @types/node package:

Choosing between CommonJS and ESM for Node.js projects

Both module systems have their advantages and trade-offs, and the choice often depends on factors like project requirements, ecosystem compatibility, and personal or team preferences. Here are some reasons why you might opt for CommonJS:

  1. Ecosystem Compatibility:
    • The Node.js ecosystem has traditionally used CommonJS modules, and many existing npm packages are written in CommonJS. If your project heavily depends on existing npm modules that are not yet converted to ESM or are not compatible with ESM, you might choose to stick with CommonJS.
  2. Tooling and Libraries:
    • Some tools and libraries in the Node.js ecosystem are designed with CommonJS in mind. If you rely on specific tools or libraries that work seamlessly with CommonJS, it might be easier to start your project with CommonJS to avoid potential compatibility issues.
  3. Incremental Adoption:
    • If you are working on an existing project or transitioning from an older version of Node.js that primarily uses CommonJS, you might choose to adopt ESM gradually. Starting with CommonJS allows for an incremental transition, enabling you to update individual modules to ESM as needed.
  4. Maturity and Stability:
    • CommonJS has been a stable and widely used module system in Node.js for many years. It has a mature ecosystem, and developers are familiar with its conventions. If stability and a well-established ecosystem are crucial for your project, starting with CommonJS might be a pragmatic choice.
  5. Simplicity and Familiarity:
    • CommonJS syntax is simpler and might be more familiar to developers who have been working with Node.js for an extended period. If your team values simplicity and ease of use, and you don't need the advanced features introduced by ESM, CommonJS might be a straightforward choice.
  6. Tooling Support:
    • While tooling support for ESM is improving, CommonJS is still more widely supported by various development tools, including bundlers, transpilers, and testing frameworks. If you rely on a specific set of tools that work well with CommonJS, it can influence your choice.

It's essential to note that Node.js itself fully supports both CommonJS and ESM, and the choice between them often depends on project-specific considerations. Additionally, Node.js continues to evolve, and the ecosystem is gradually adapting to embrace ESM more widely. When starting a new project, it's a good idea to evaluate the specific requirements and constraints and choose the module system that aligns best with your needs.

TS2354: This syntax requires an imported helper but module  tslib  cannot be found.

I came across this error when using async / await in ts file module and tsconfig.json like this:

  "compilerOptions": {
    "module": "ESNext",
    "moduleResolution": "esnext",

tslib is commonly used to support certain JavaScript features, including the transformation of async/await code.

Try adding:

npm install tslib

And this ensures that TypeScript includes the necessary library for supporting async/await:

// tsconfig.json
{
  "compilerOptions": {
    "lib": ["esnext", "other-libraries"],
    // other options...
  },
  // other settings...
}

Alternatively, the error disappears after I switched to

  "compilerOptions": {
    "module": "NodeNext",
    "moduleResolution": "nodenext",

Do I need to specify file extensions when importing modules?

The need to specify file extensions in import statements depends on your runtime and compilation environment, and also on whether you are using ES modules (the import syntax) or CommonJS modules (the require syntax)

  • Webpack (used by Create React App and others tools): If the path has a file extension, then the file is bundled straightaway. Otherwise, the file extension is resolved using the resolve.extensions option, which tells the resolver which extensions are acceptable for resolution, e.g., .js.jsx.
  • ES modules with Node.js or in the browser without any compilation step: A file extension must be provided when using the import keyword to resolve relative or absolute specifiers. This behavior matches how import behaves in browser environments.
  • CommonJS modules with Node.js: If the exact filename is not found, then Node.js will attempt to load the required filename with the added extensions: .js.json, and finally .node.
  • TypeScript: When using '--moduleResolution' with the option 'nodenext', it is necessary to add explicit file extensions to relative import paths in EcmaScript imports. However, TypeScript was implemented based on the import syntax proposal before ES6 was finalized, so they followed node module (commonjs) behavior which guesses file extensions.

How does Webpack module resolution play together with TypeScript module resolution?

Webpack and TypeScript use different module resolution strategies, but they can work together seamlessly to build and bundle projects. Understanding how each tool handles module resolution is crucial for efficient development.

  1. TypeScript Module Resolution:
    • TypeScript has its own module resolution logic, which includes resolving module paths based on the baseUrl, paths, and rootDirs settings in the tsconfig.json file.
    • TypeScript can resolve both relative and non-relative module paths using these configurations.
    • TypeScript's resolution is mainly used during the TypeScript compilation process to generate valid JavaScript code.
    • TypeScript supports different module systems, including CommonJS and ECMAScript Modules (ESM).
  2. Webpack Module Resolution:
    • Webpack also has its module resolution logic, which is responsible for locating and bundling modules during the build process.
    • Webpack can handle a variety of module formats, including CommonJS, AMD, and ES6 modules (ESM).
    • It uses the resolve configuration to determine how to locate and load modules, including specifying file extensions, aliasing paths, and defining module directories.
    • Webpack's module resolution is applied at runtime when the bundled application is executed in the browser or another runtime environment.
  3. Working Together:
    • During development, TypeScript and Webpack can complement each other. TypeScript handles type checking and transpilation, and Webpack bundles the compiled JavaScript files along with their dependencies for deployment.
    • Webpack extends the module resolution capabilities of TypeScript by allowing for additional configurations and optimizations during the bundling process.
    • Ensure that TypeScript and Webpack configurations are aligned to avoid conflicts. For example, file extensions and paths configured in Webpack's resolve should match TypeScript's settings to prevent unexpected resolution issues.
  4. Common Configurations:
    • It's a good practice to have common configurations for paths, extensions, and aliases in both TypeScript and Webpack to maintain consistency and prevent potential issues.
  5. Webpack ts-loader:
    • When using TypeScript with Webpack, the ts-loader is commonly employed to integrate TypeScript compilation into the Webpack build process. This loader utilizes the TypeScript compiler to transpile TypeScript code and works in harmony with Webpack's module resolution.
  6. Using Webpack Aliases in TypeScript:
    • If you have defined aliases in Webpack's resolve.alias, you might want to mirror them in TypeScript using paths in tsconfig.json to avoid discrepancies in module resolution.

How do I reference modules that have a custom TypeScript path?

To reference modules with a custom TypeScript path, you typically use the paths configuration in the tsconfig.json file. This allows you to define custom paths that map to specific directories, making it easier to reference and import modules in your TypeScript code.

Update tsconfig.json:

  • Open your tsconfig.json file and add or update the paths property to include your custom paths. The paths property is an object where keys represent the module names, and values represent the corresponding paths.
  • The baseUrl configuration in tsconfig.json specifies the base directory for resolving non-relative module names. Make sure it is set appropriately for your project.
// tsconfig.json
{
  "compilerOptions": {
    "baseUrl": "./src",
    "paths": {
      "@myModule/*": ["app/*"],
      "@utils/*": ["utils/*"]
      // Add more custom paths as needed
    },
    // Other compiler options...
  },
  // Other settings...
}

Organize your project structure according to the defined paths. For example, if you have the following directory structure:

src/
  app/
    moduleA.ts
    moduleB.ts
  utils/
    utilityModule.ts

In your TypeScript files, you can now use the custom paths to reference and import modules.

// In a file in the app directory
import { something } from '@myModule/moduleA';

// In a file in the utils directory
import { utilityFunction } from '@utils/utilityModule';

Webpack or Other Build Tools:

  • If you're using a bundler like Webpack, ensure that the bundler's configuration also recognizes and respects the custom paths. Webpack has its own configuration for module resolution (resolve.alias), and it should be aligned with the TypeScript configuration.

About Author

Mathias Bothe To my job profile

I am Mathias, born 40 years ago in Heidelberg, Germany. Today I am living in Munich and Stockholm. I am a passionate IT freelancer with more than 16 years experience in programming, especially in developing web based applications for companies that range from small startups to the big players out there. I am founder of bosy.com, creator of the security service platform BosyProtect© and initiator of several other software projects.