Module Resolution: ESModules vs CommonJS in TypeScript

Table of Contents

  • Introduction
  • What is Module Resolution?
  • CommonJS Modules
  • ESModules (ESM)
  • Module Resolution in TypeScript
    • TypeScript Module Resolution Process
    • Module Resolution Strategies
  • ESModules vs CommonJS in TypeScript
    • Syntax Differences
    • Import/Export Compatibility
    • Default Exports
    • Interoperability Between ESM and CommonJS
  • Conclusion

Introduction

In modern JavaScript and TypeScript development, modular code is a fundamental building block. However, there are different ways to structure modules, which leads to different module systems. The two most prominent module systems in JavaScript today are CommonJS (CJS) and ESModules (ESM).

TypeScript, as a statically typed superset of JavaScript, supports both CommonJS and ESModules but requires specific handling to resolve and compile modules correctly. This article will explore the differences between CommonJS and ESModules in TypeScript, how module resolution works, and how these two systems interact.


What is Module Resolution?

Module resolution is the process by which TypeScript determines how to find and load external modules or packages in your project. This process is particularly important when using import and export syntax in your TypeScript code, as TypeScript needs to know how to locate the correct files, handle their types, and compile them into JavaScript.

Types of Module Systems

  • CommonJS (CJS): Primarily used in Node.js environments. Modules are typically imported using require() and exported using module.exports or exports.
  • ESModules (ESM): The standard JavaScript module system that is now supported in modern JavaScript environments, including browsers and Node.js. Modules are imported using the import and export syntax.

CommonJS Modules

CommonJS is the traditional module system used by Node.js and was introduced before the official ESModules specification. CommonJS modules use require() to import dependencies and module.exports or exports to export functionality.

Example of CommonJS Syntax:

Module 1: math.js

// Exporting in CommonJS
module.exports.add = function (a, b) {
return a + b;
};
module.exports.subtract = function (a, b) {
return a - b;
};

Module 2: app.js

// Importing in CommonJS
const math = require('./math');

console.log(math.add(5, 3)); // Output: 8

Key Characteristics of CommonJS:

  • Synchronous Loading: Modules are loaded synchronously, which works well in server environments like Node.js but can cause issues in browsers with many modules.
  • Exports: Modules export using module.exports or exports, with support for objects, functions, or primitives.

ESModules (ESM)

ESModules (ESM) is the modern standard for JavaScript modules. Introduced in ECMAScript 6 (ES6), it is now the preferred method for structuring code in both client-side and server-side JavaScript. ESModules use the import and export keywords to define dependencies and share functionality.

Example of ESModule Syntax:

Module 1: math.mjs

// Exporting in ESModules
export function add(a, b) {
return a + b;
}

export function subtract(a, b) {
return a - b;
}

Module 2: app.mjs

// Importing in ESModules
import { add, subtract } from './math.mjs';

console.log(add(5, 3)); // Output: 8

Key Characteristics of ESModules:

  • Asynchronous Loading: ESModules are designed to be loaded asynchronously, which is especially beneficial for front-end applications and modern bundling tools.
  • Syntax: Uses import and export to load and expose modules.
  • Static Analysis: ESModules are statically analyzable, allowing better optimization by bundlers and tools.

Module Resolution in TypeScript

TypeScript uses a module resolution strategy to figure out how to load and resolve dependencies. TypeScript supports multiple module systems, including CommonJS and ESModules.

TypeScript Module Resolution Process

When TypeScript compiles a project, it follows a set of steps to resolve module imports:

  1. File Lookup: TypeScript first tries to find the module in the node_modules folder or the paths specified in the tsconfig.json file.
  2. Extension Lookup: TypeScript checks for .ts, .tsx, .d.ts, .js, or .jsx extensions in that order when resolving modules.
  3. Fallback Strategy: If no specific file is found, TypeScript looks for index files like index.ts or index.js.

Module Resolution Strategies

  • Classic: This is the legacy resolution strategy that mimics how modules were resolved in earlier versions of TypeScript (pre-Node.js support).
  • Node: This strategy follows the module resolution logic that Node.js uses, making it ideal for server-side applications. It supports looking in node_modules, resolving relative paths, and handling package.json configurations like main and exports.

To enable the Node resolution strategy, you need to specify it in the tsconfig.json file:

{
"compilerOptions": {
"moduleResolution": "node"
}
}

ESModules vs CommonJS in TypeScript

Syntax Differences

The most obvious difference between CommonJS and ESModules is the syntax used to import and export modules.

  • CommonJS uses require() to import and module.exports to export.
  • ESModules uses import and export for module handling.

Example:

// CommonJS
const { add } = require('./math');

// ESModule
import { add } from './math';

Import/Export Compatibility

ESModules and CommonJS are not natively compatible with each other. TypeScript provides mechanisms to handle interoperation, but there are some important differences:

  • CommonJS exports are objects, which means that you can’t directly import named exports as you would in ESModules. You would typically import the entire module and destructure it. // Importing CommonJS module in ESM style import * as math from './math';
  • ESModules default exports can be tricky when interoperating with CommonJS, because CommonJS modules export an object that contains all of the module’s exports. TypeScript will handle this with esModuleInterop flag.

To enable better interoperability between the two, you can use the esModuleInterop compiler option:

{
"compilerOptions": {
"esModuleInterop": true
}
}

This ensures smoother compatibility, especially when dealing with default exports.

Default Exports

In ESModules, default exports are explicitly marked with the default keyword. However, CommonJS does not have a formal concept of default exports and treats the entire module as an export object.

For example:

  • ESModules (Default export): // math.ts (ESM) export default function add(a: number, b: number): number { return a + b; }
  • CommonJS (Equivalent): // math.js (CJS) module.exports = function add(a, b) { return a + b; };

Interoperability Between ESM and CommonJS

TypeScript can manage interoperability between ESM and CommonJS with some configuration. For example, when working with a CommonJS module in an ESModule-based project, TypeScript requires you to use import syntax even if the CommonJS module is being used.

// Using CommonJS module in ESModule-style code
import * as math from './math'; // Works with esModuleInterop

TypeScript Configuration

In tsconfig.json, you can set the module system and resolution strategy to control how TypeScript resolves modules:

{
"compilerOptions": {
"module": "ESNext", // Use "ESNext" for ESModules or "CommonJS" for CJS
"moduleResolution": "node", // Enable Node.js module resolution
"esModuleInterop": true // Allow CommonJS modules to be imported with ESModule syntax
}
}

Conclusion

Understanding module resolution and the differences between ESModules and CommonJS in TypeScript is crucial for managing dependencies and building scalable applications. TypeScript allows developers to seamlessly use both module systems, but understanding their syntax, default export handling, and interoperation between the two is key to writing type-safe code.

When working with TypeScript, it’s essential to choose the right module system based on your project requirements. For modern web development, ESModules are preferred, while CommonJS is more suitable for Node.js server-side code.

By properly configuring your TypeScript project and understanding these module systems, you can ensure smooth module resolution and interoperability between different JavaScript libraries and projects.