Module Resolution Strategies in TypeScript
Overview
A module is a way to create a group of related variables, classes, interfaces, functions, etc. It is executed in the local scope, not in the global scope. A module is created by using the export keyword and can be used in other modules by using the import keyword. A module can be imported by another module using the module loader. Module loader is responsible for finding and executing all the dependencies of a module before executing the loader.
What are Module Resolution Techniques in TypeScript?
Module Resolution is a process the compiler uses to figure out what an import refers to. If we consider an import statement like import { x } from "moduleX", as we need to check the use of x, the compiler needs to know the representation, and then the compiler checks the definition moduleX. The moduleX could be defined in one of the .ts/.tsx files, or in a .d.ts that depends on our code.
The compiler will try to locate the file that represents the imported module. To do this, the compiler follows two different strategies: Classic and Node. These strategies help the compiler to figure out the path of moduleX.
If the strategies didn't work, then the compiler will try to locate an ambient modular declaration. If the compiler could not resolve the module, an error will be thrown and the error will look like this: error TS2307: Cannot find module 'moduleX'.
Types of Module Resolution Techniques in TypeScript?
Module Imports are resolved differently depending on whether the module reference is relative or non-relative. So, there are two types of module resolution techniques: Relative Imports and Non-Relative Imports.
-
Relative Imports:
A relative import is defined as resolved relative to the importing file and cannot resolve to an ambient modular declaration. We should use the relative modules for our modules that guarantee to maintain their relative location at the runtime.
A relative import is one that starts with /, ./ or ../. Here is the syntax to declare a relative import:
-
Non-Relative Imports:
A non-relative import is defined as resolved relative to the baseUrl, or through path mapping. They can also resolve to ambient modular declarations. We can use non-relative paths when we import any of our external dependencies.
The imports that are not relative will be considered non-relative imports. Here is the syntax to declare a non-relative import:
Classic
When It is Used?
The classic strategy is used to be TypeScript's default resolution strategy. This strategy is majorly used for backward compatibility.
Classic Strategy for Relative Imports
A relative import in TypeScript can be resolved relative to the importing file.
Implementation and Example
The import { x } from "./moduleX" in the source file /root/src/folder/X.ts will result in the following lookups:
- /root/src/folder/moduleX.ts
- /root/src/folder/moduleX.d.ts
Classic Strategy for Non-Relative Imports
For non-relative imports, the compiler walks through the directory tree starting with the directory containing the importing file, which tries to locate a matching definition file.
Implementation and Example
A non-relative import to moduleX such as import { x } from "moduleX" in a source file /root/src/folder/Y.ts will result in attempting the following locations to locate "moduleX":
- /root/src/folder/moduleX.ts
- /root/src/folder/moduleX.d.ts
- /root/src/moduleX.ts
- /root/src/moduleX.d.ts
- /root/moduleX.ts
- /root/moduleX.d.ts
- /moduleX.ts
- moduleX.d.ts
Node
When It is Used?
This resolution strategy attempts to take off the Node.js module resolution mechanism at runtime.
Node Strategy for Relative Imports
Imports in Node.js are performed by calling a function named require. Relative paths are considered straightforward paths.
Implementation and Example
let's consider a file located at /root/src/moduleX.js, that contains the import var x = require("./moduleY");, Node.js resolves that import in the following order:
- Search the file named /root/src/moduleY.js if the file exists.
- Search the folder /root/src/moduleY if the folder contains a file named package.json that specifies a "main" module. If Node.js found the file /root/src/moduleY/package.json that contains {"main": "lib:/mainModule.js"}, then Node.js refers to /root/src/moduleY/lib/mainModule.js.
- Search the folder /root/src/moduleY if the folder contains a file named index.js that file is implicitly considered that folder's "main" module.
Node Strategy for Non-Relative Imports
In this, Node will search for modules in special folders named node_modules. A node_modules folder can be of the same level as the current file, or higher up in the directory chain. The node will walk up the directory chain, and look through each node_modules until the node finds the module we tried to load.
Implementation and Example
Now, consider if root/src/moduleX.js instead used a non-relative path and had the import var x = require("moduleY");, Node will try to resolve moduleY to each of the locations until one worked.
- /root/src/node_modules/moduleY.js
- /root/src/node_modules/moduleY/index.js
- /root/node_modules/moduleY.js
- /root/node_modules/moduleY/index.js
- /node_modules/moduleY.js
- /node_modules/moduleY/index.js
When we specify a main property, the below paths are used:
- /root/src/node_modules/moduleY/package.json
- /root/node_modules/moduleY/package.json
- /node_modules/moduleY/package.json
Conclusion
- Modules, variables, classes, interfaces, etc. run on their scope, not on the global scope.
- TypeScript has the same module concept as the ES6 module. Modules can have both declarations and code.
- A module can be imported by another module using the module loader.
- We can use the import statement to access exports from other modules and the export statement to export variables, functions, classes, and interfaces from a module.
- The node module resolution is the most common process recommended to handle large TypeScript projects.
- If there are resolution problems with import s and export s in TypeScript, we can try setting the moduleResolution: "node" to fix this problem.
- We can use the noResolve compiler options to instruct the compiler not to add any files to the compilation that were passed onto the command line.