Module resolution techniques and strategies in TypeScript

Modules are an essential concept in TypeScript and JavaScript, which allow us to organize and encapsulate code. TypeScript provides module resolution techniques to determine how import statements are resolved, where to find the referenced modules, and how module resolution strategies help load the required modules. Here, we will explore different techniques and strategies to understand how modules work in TypeScript.

Module basics

Before getting into module resolution techniques, let’s go through some module-related concepts:

  • Modules definition: Modules combine all variables, classes, functions, etc., into a self-contained unit or container. This unit operates in the local scope rather than the global scope.

  • Export and import: In TypeScript, you use the export keyword to make entities Variables, functions, classes, etc.available in other modules. The import keyword is used to bring entities from outer modules.

  • Scope: Modules in TypeScript encapsulate code within isolated scopes, preventing pollution of the global namespace. This isolation is a crucial step in code organization, making it easier to assemble complex applications while minimizing the risk of naming conflicts and improving code maintainability.

Module resolution techniques

Module resolution is, by definition, finding and loading modules that our code depends on. TypeScript uses two main module resolution techniques: Relative imports and non-relative imports.

Relative imports

Relative imports are resolved relatively to the importing file. They are well-suited for modules that maintain their relative location at runtime. Relative imports mostly have path prefixes like ./, ../, or /mod to specify the module’s location.

How do relative imports work?
How do relative imports work?

Syntax

Here is the syntax for relative imports:

//Method#1
import Entry from "./components/Entry";
//Method#2
import { DefaultHeaders } from "../constants/http";
//Method#3
import "/mod";
Syntax for relative imports

Now we will go through an example of relative module resolution techniques to understand the concept better:

{
  "compilerOptions": {
    "target": "ES6",
    "module": "CommonJS",
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules"]
}
Example of relative module resolution techniques

Here is an explanation:

  • Line 1: Uses the import function to import two functions add and subtract from ../utils/math.ts, which is a relative import path, indicating the import of functions from a module located in the parent directory (..) followed by the utils subdirectory and the math.ts file.

  • Line 3: Declares a variable result1 to store the results of add(5,3).

  • Line 4: Declares a variable result2 to store the results of subtract(10,4/////).

  • Line 6: Uses console.log to print a message to the console that contains a message "Addition result: " and ${result1}, a placeholder that will be replaced with the value of the result1 variable when the string is constructed.

  • Line 7: Uses console.log to print a message to the console that contains a message "Subtraction result: " and ${result2}, a placeholder that will be replaced with the value of the result2 variable when the string is constructed.

Non-relative imports

Non-relative imports are resolved relatively to the baseUrl specified in the tsconfig.json or through path mapping. They can also be resolved to modular declarations. Non-relative imports are mostly used for external dependencies and for modules that are not part of a particular project. Considering the example of lodash library, we can explain how non-relative imports are used.

How do non-relative imports work?
How do non-relative imports work?

Syntax

Here is the syntax for non-relative imports:

import * as $ from "library"
Syntax for non-relative import

Now we will go through an example of non-relative module resolution techniques to understand the concept better:

{
  "compilerOptions": {
    "target": "ES6",
    "module": "CommonJS",
    "outDir": "./dist"
  },
  "include": ["*.ts"],
  "exclude": ["node_modules"]
}
Example of non-relative module resolution techniques

Here is an explanation:

  • Line 1: Uses the import function to import the entire lodash library and assign it an alias lsh. This allows us to use lodash functions by referencing them via lsh.

  • Line 3: Declares a constant variable named numbers and initializes it with an array containing five numbers: 1, 2, 3, 4, and 5.

  • Line 4: Declares a constant variable sum, that is used to store the result of lsh.sum(). The sum() function from lodash library is called and numbers is passed as an argument and sum of values in numbers is stored in sum variable.

  • Line 6: Uses the console.log to print a message to the console that contains a message "Sum of numbers: " and ${sum}, a placeholder that will be replaced with the value of the sum variable when the string is constructed.

Now that we have understood the different module resolution techniques used in the TypeScript, we will proceed further and learn about different module resolution strategies used in TypeScript.

Classic vs. Node strategies

TypeScript involves two main strategies for module resolution: Classic and Node. The choice between these strategies depends on the project configuration and compatibility requirements. To specify the module resolution strategy, set the moduleResolution option in the tsconfig.json to classic or node.

Syntax

Here is the syntax for defining module resolution strategy in tsconfig.json.

{
"compilerOptions": {
"moduleResolution": "node"
}
}
Set the moduleResolution option to "node"

In the classic strategy, we typically see both TypeScript and JavaScript files in the output directory, while in the node strategy, TypeScript files remain unchanged, and JavaScript files have the same name but are recognized as JavaScript modules. In the following diagram, we can see how the module resolution strategies actually work:

Module resolution strategies in Typescript
Module resolution strategies in Typescript

Classic strategy

It is TypeScript's default resolution strategy and is used for backward compatibility. Classic strategy follows particular relative and non-relative import resolution paths. Here's how we can present the classic strategy:

{
  "compilerOptions": {
    "target": "ES6",
    "module": "CommonJS",
    "outDir": "./dist-classic"
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules"],
  "esModuleInterop": true
}
Example of classic module resolution strategy

Here is an explanation:

  • Line 2: "compilerOptions": { ... } specifies various compiler options for TypeScript.

  • Line 3: "target": "ES6" sets the ECMAScript version to target when compiling TypeScript code.

  • Line 4: "module": "CommonJS" specifies the module system to use when generating JavaScript.

  • Line 5: "outDir": "./dist-classic" specifies the output directory for the compiled JavaScript files.

  • Line 7: "include": ["src/**/*.ts"] defines which TypeScript files should be included for compilation.

  • Line 8: "exclude": ["node_modules"] specifies which directories or files should be excluded from compilation.

  • Line 9: "esModuleInterop": true enables ES module interop. It allows TypeScript to generate code that works seamlessly with both ES modules (import/export syntax) and CommonJS modules (require/module.exports syntax).

Node strategy

It is a strategy that emulates the Node.js module resolution phenomena. It is used when we have to align your TypeScript project with Node.js conventions for imports and resolution paths. Here's how we can use Node strategy:

{
  "compilerOptions": {
    "target": "ES6",
    "module": "CommonJS",
    "outDir": "./dist-node",
    "moduleResolution": "node"
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules"],
  "esModuleInterop": true
}
Example of node module resolution strategy

Here is an explanation:

  • Line 2: "compilerOptions": { ... } specifies various compiler options for TypeScript.

  • Line 3: "target": "ES6" sets the ECMAScript version to target when compiling TypeScript code.

  • Line 4: "module": "CommonJS" specifies the module system to use when generating JavaScript.

  • Line 5: "outDir": "./dist-classic" specifies the output directory for the compiled JavaScript files.

  • Line 7: "include": ["src/**/*.ts"] defines which TypeScript files should be included for compilation.

  • Line 8: "exclude": ["node_modules"] specifies which directories or files should be excluded from compilation.

  • Line 9: "esModuleInterop": true enables ES module interop. It allows TypeScript to generate code that works seamlessly with both ES modules (import/export syntax) and CommonJS modules (require/module.exports syntax).

  • Line 10: "moduleResolution": "node" controls how TypeScript resolves module imports. Setting it to "node" means that TypeScript will use Node.js-style module resolution, where it looks for modules in the "node_modules" directory and follows the Node.js module resolution algorithm.

Module resolution techniques are important for managing dependencies and organizing code, especially in TypeScript projects. Understanding the distinction between relative and non-relative imports, as well as the choice between classic and Node strategies, can help us create more applications.

Copyright ©2024 Educative, Inc. All rights reserved