Module Resolution
Module Resolution
Module parsing is the process used by the compiler to determine the import of the referred content. Consider import statements like import { a } from "moduleA";. In order to check any use of a, the compiler needs to know exactly what it represents, and needs to check its definition of moduleA.
At this point, the compiler will ask "What is the shape of moduleA?" Although this sounds simple, moduleA can be defined in one of your own .ts/.tsx files, or in the . that your code depends on.
First, the compiler will try to locate a file representing the imported module. To do this, the compiler follows one of two different strategies: classical or Node. These strategies tell the compiler where to look for moduleA.
If this does not work and the module name is non-related (in the case of "moduleA", it is), the compiler will try to locate an environment module declaration. Next we will introduce non-relative imports.
Finally, if the compiler cannot parse the module, it will log an error. In this case, the error is similar to error TS2307: module 'moduleA' not found.
Relative vs. Non-relative module imports
Depending on whether the module reference is relative or non-relative, the analysis method of module import is different.
Relative imports are imports starting with /, ./ or ../. Some examples include:
import Entry from "./components/Entry"; import { DefaultHeaders } from "../constants/http"; import "/mod";
Any other import is considered non-related. Some examples include:
import * as $ from "jquery"; import { Component } from "@angular/core";
Relative import is parsed relative to the import file and cannot be parsed into an environment module declaration. You should use relative imports for your modules to ensure that they maintain their relative position at runtime.
Module Resolution Strategies
There are two possible module resolution strategies: Node and Classic. You can use the --moduleResolution flag to specify a module resolution policy. If not specified, --module commonjs defaults to Node, otherwise defaults to Classic (including when --module is set to amd, system, umd, es2015, esnext, etc.).
Note: Node module parsing is the most commonly used in the TypeScript community and is recommended for most projects. If you are having parsing issues with import and export in TypeScript, try setting moduleResolution: "node" to see if it resolves the issue.
Classical parsing strategy
This used to be the default parsing policy for TypeScript. Today, this strategy is mainly for backward compatibility.
Relative imports will be parsed relative to the imported file. Therefore, import { b } from "./moduleB" in the source file /root/src/folder/ will result in the following searches:
- /root/src/folder/
- /root/src/folder/
However, for non-related module imports, the compiler starts from the directory containing the import files and walks up the directory tree, trying to find a matching definition file.
For example:
In the source file /root/src/folder/, a non-relative import of moduleB, such as import { b } from "moduleB", will cause an attempt to locate "moduleB" using the following locations:
/root/src/folder/ /root/src/folder/ /root/src/ /root/src/ /root/ /root/ / /
Node mode
This parsing strategy attempts to mimic the module parsing mechanism at runtime. The complete parsing algorithm is outlined in the module documentation.
How to parse modules?
To understand the modules, it is very important to understand what steps the TS compiler will follow. Traditionally, imports in _ are performed by calling a function called require. The behavior taken will vary depending on whether the require is a relative or non-relative path.
The relative path is quite simple. For example, let's consider a file located in /root/src/ with import var x = require("./moduleB");
The import is parsed in the following order:
- Ask if the file named /root/src/ exists.
- Ask the folder /root/src/moduleB whether it contains a file named "main" module that specifies the "main" module. In our example, if the file /root/src/moduleB/ is found to contain { "main": "lib/" }, then /root/src/moduleB/lib/ will be referenced.
- Ask the folder /root/src/moduleB if it contains a file named . This file is implicitly treated as the "main" module of the folder.
However, parsing the non-related module names is performed in different ways. Node will look for your module in a special folder called node_modules. The node_modules folder can be at the same level as the current file or at a higher level in the directory chain. Node will traverse upward along the directory chain, viewing each node_modules until the module you are trying to load is found.
Following our example above, consider whether /root/src/ uses a non-relative path and import var x = require("moduleB");. Node then tries to parse moduleB to each location until one location works properly.
(1) /root/src/node_modules/
(2) /root/src/node_modules/moduleB/ (if it specifies the "main" attribute)
(3) /root/src/node_modules/moduleB/
(4) /root/node_modules/
(5) /root/node_modules/moduleB/ (if it specifies the "main" attribute)
(6) /root/node_modules/moduleB/
(7) /node_modules/
(8) /node_modules/moduleB/ (if it specifies the "main" attribute)
(9) /node_modules/moduleB/
Note that a directory is jumped in steps (4) and (7).
You can read more about loading modules from node_modules in the documentation.
How TypeScript resolves modules
TypeScript will mimic the runtime parsing policy to locate the module's definition files at compile time. To do this, TypeScript overwrites the TypeScript source file extensions (.ts, .tsx, and .) on the parsing logic of Node. TypeScript will also use a field named "types" in , to reflect the purpose of "main" - the compiler will use it to find the "main" definition file to be looked up.
For example, an import statement like import { b } from "./moduleB" in /root/src/ will cause an attempt to locate "./moduleB":
(1)/root/src/
(2)/root/src/
(3)/root/src/
(4)/root/src/moduleB/ (if it specifies the "types" attribute)
(5)/root/src/moduleB/
(6)/root/src/moduleB/
(7)/root/src/moduleB/
Recall, look for a file named , then the applicable , and then .
Likewise, non-relative imports will follow parsing logic, first looking for files and then looking for applicable folders. Therefore, import { b } from "moduleB" in the source file /root/src/ will result in the following searches:
/root/src/node_modules/ /root/src/node_modules/ /root/src/node_modules/ /root/src/node_modules/moduleB/(If it specifies“types”property) /root/src/node_modules/@types/ /root/src/node_modules/moduleB/ /root/src/node_modules/moduleB/ /root/src/node_modules/moduleB/ /root/node_modules/ /root/node_modules/ /root/node_modules/ /root/node_modules/moduleB/(If it specifies“types”property) /root/node_modules/@types/ /root/node_modules/moduleB/ /root/node_modules/moduleB/ /root/node_modules/moduleB/ /node_modules/ /node_modules/ /node_modules/ /node_modules/moduleB/(If it specifies“types”property) /node_modules/@types/ /node_modules/moduleB/ /node_modules/moduleB/ /node_modules/moduleB/
Don't be intimidated by the number of steps here - TypeScript still only jumps to the directory twice in steps (9) and (17). This is actually no more complicated than what it does.
Additional module resolution flags
The project source layout sometimes does not match the output layout. Usually a set of build steps generates the final output. These include compiling the .ts file to .js, and copying dependencies from different source locations to a single output location. The end result is that the module's name at runtime may be different from the name of the source file that contains its definition. Or the module path in the final output may not match its corresponding source file path at compile time.
The TypeScript compiler has an additional set of flags to notify the compiler of the conversion expected to occur on the source to generate the final output.
It should be noted that the compiler does not perform any of these transformations; it simply uses this information to guide the process of importing and parsing modules into their definition files.
Base Url
In applications that use AMD module loaders, using baseUrl is a common practice where modules are "deployed" to a single folder at runtime. The source code of these modules can be located in different directories, but the build script will put them together.
Set the baseUrl to inform the compiler where to find the module. It is assumed that all module imports with non-relative names are related to baseUrl.
The value of baseUrl is determined as:
(1) The value of the baseUrl command line parameter (calculated based on the current directory if the given path is relative)
(2) The value of the baseUrl property in ''' (if the given path is relative, it is calculated based on the position of '')
Note that relative module imports are not affected by setting baseUrl, as they are always parsed relative to their import files.
You can find more documentation about baseUrl in the RequireJS and SystemJS documentation.
path mapping
Sometimes the module is not directly located under baseUrl. For example, the import of the module "jquery" will be converted to "node_modules/jquery/dist/" at runtime. The loader uses a mapping configuration to map module names to files at runtime, see the RequireJs documentation and the SystemJS documentation.
The TypeScript compiler supports declaring such mappings using the "paths" attribute in a file. Here is an example of how to specify the "paths" attribute for jquery.
{ "compilerOptions": { "baseUrl": ".", // This must be specified if "paths" is. "paths": { "jquery": ["node_modules/jquery/dist/jquery"] // This mapping is relative to "baseUrl" } } }
How to check the TypeScript module parsing process
As mentioned earlier, the compiler can access files outside the current folder when parsing the module. This can be difficult when diagnosing the reason why the module is not parsed or resolved to an incorrect definition. Enabling compiler module resolution traces with --traceResolution provides insight into what happens during module resolution.
Suppose we have a sample application using the typescript module.
has an import like import * as ts from "typescript".
│ ├───node_modules │ └───typescript │ └───lib │ └───src
Compile using the following command line:
tsc --traceResolution
result:
======== Resolving module 'typescript' from 'src/'. ========
Module resolution kind is not specified, using 'NodeJs'.
Loading module 'typescript' from 'node_modules' folder.
File 'src/node_modules/' does not exist.
File 'src/node_modules/' does not exist.
File 'src/node_modules/' does not exist.
File 'src/node_modules/typescript/' does not exist.
File 'node_modules/' does not exist.
File 'node_modules/' does not exist.
File 'node_modules/' does not exist.
Found '' at 'node_modules/typescript/'.
'' has 'types' field './lib/' that references 'node_modules/typescript/lib/'.
File 'node_modules/typescript/lib/' exist - use it as a module resolution result.
======== Module name 'typescript' was successfully resolved to 'node_modules/typescript/lib/'. ========
The source code location that triggers module resolution:
======== Resolving module ‘typescript’ from ‘src/’. ========
Module parsing strategy:
Module resolution kind is not specified, using ‘NodeJs’.
Loading of types from npm packages:
‘’ has ‘types’ field ‘./lib/’ that references ‘node_modules/typescript/lib/’.
The final successful parsed output:
======== Module name ‘typescript’ was successfully resolved to ‘node_modules/typescript/lib/’. ========
The above is the detailed content of the TypeScript Module Resolution analysis process. For more information about TypeScript Module Resolution, please follow my other related articles!