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
exportkeyword to make available in other modules. Theentities Variables, functions, classes, etc. importkeyword 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.
Syntax
Here is the syntax for relative imports:
//Method#1import Entry from "./components/Entry";//Method#2import { DefaultHeaders } from "../constants/http";//Method#3import "/mod";
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"]
}
Here is an explanation:
Line 1: Uses the
importfunction to import two functionsaddandsubtractfrom../utils/math.ts, which is a relative import path, indicating the import of functions from a module located in the parent directory (..) followed by theutilssubdirectory and themath.tsfile.Line 3: Declares a variable
result1to store the results ofadd(5,3).Line 4: Declares a variable
result2to store the results ofsubtract(10,4/////).Line 6: Uses
console.logto print a message to the console that contains a message "Addition result: " and${result1}, a placeholder that will be replaced with the value of theresult1variable when the string is constructed.Line 7: Uses
console.logto print a message to the console that contains a message "Subtraction result: " and${result2}, a placeholder that will be replaced with the value of theresult2variable 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.
Syntax
Here is the syntax for non-relative imports:
import * as $ from "library"
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"]
}
Here is an explanation:
Line 1: Uses the
importfunction to import the entire lodash library and assign it an aliaslsh. This allows us to use lodash functions by referencing them vialsh.Line 3: Declares a constant variable named
numbersand 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 oflsh.sum(). Thesum()function fromlodashlibrary is called andnumbersis passed as an argument and sum of values innumbersis stored insumvariable.Line 6: Uses the
console.logto 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"}}
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:
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
}
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": trueenables 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
}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": trueenables 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.
Free Resources