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.
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 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 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 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.
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 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 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.
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 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.
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
.
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:
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": 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).
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": 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.