Introduction
Build processes in TypeScript can become quite complex when we have to configure our project flow manually through the tsconfig.json
file. That is because these configurations require understanding the TypeScript compiler and module system.
Having worked on many TypeScript projects myself, I have been able to spot two common problems that arise when using TypeScript modules and, more importantly, how to resolve them effectively.
Prerequisites
To get the most out of this article, you will want to be armed with the following:
- A strong background in JavaScript and TypeScript
- A firm understanding of TypeScript modules system
Problem 1: Irregular location of dependencies
On a normal occasion, the node-modules
directory is usually located in the root directory (i.e. baseUrl) of the project as shown below:
projectRoot ├── node_modules ├── src │ ├── file1.ts │ └── file2.ts └── tsconfig.json └── package.json
Sometimes, however, modules are not directly located under the baseUrl
. As an example, take a look at the following JavaScript code:
// index.js import express from "express";
Loaders such as webpack use a mapping configuration to map the module name (in this case, express
) to the index.js
file at runtime, thereby translating the snippet above to node_modules/express/lib/express
at run-time.
At this point, when we use the translated snippet above in a TypeScript project, we must then configure the TypeScript compiler to handle the module import using the "paths"
property:
// tsconfig.json { "compilerOptions": { "baseUrl": "./src", "paths": { "express": ["node_modules/express/lib/express"] } } }
However, with the above configuration, TypeScript compiler will throw the following error:
express module not found
Here’s what’s happening under the hood: TypeScript compiler searches for node_modules
in the src
directory even though node_modules
is located outside the src
directory, thereby determining that the module was not found.
Solution 1: Locate the correct directory
The mapping in "paths"
is resolved relative to "baseUrl"
. Hence, our configuration should be as follows:
// tsconfig.json { "compilerOptions": { "baseUrl": "./src", // This must be specified if "paths" is. "paths": { "express": ["../node_modules/express/lib/express"] // This mapping is relative to "baseUrl" } } }
When we use this configuration, TypeScript compiler “jumps” up a directory from the src
directory and locates the node_modules
directory.
Alternatively, the configuration below is also valid:
// tsconfig.json { "compilerOptions": { "baseUrl": ".", // This must be specified if "paths" is. "paths": { "express": ["node_modules/express/lib/express"] // This mapping is relative to "baseUrl" } } }
When we use this configuration, TypeScript compiler will search for the node_modules
directory in the root directory of the project.
Problem 2: Multiple fallback locations
Our second common issue also has to do with location. Let’s consider the following project configuration:
projectRoot ├── view │ ├── file1.ts (imports 'view/file2' and 'nav/file3') │ └── file2.ts ├── components │ ├── footer │ └── nav │ └── file3.ts └── tsconfig.json
Here, the view/file2
module is located in the view directory and nav/file3
in the components directory.
Resolving modules in multiple locations can be a bit challenging because at this point in your code, the compiler does not know how to resolve these modules from different locations. In the following section, we will review how to resolve this issue.
Solution 2: Locate the module and resolve imports
Using the configuration below, we can tell the compiler to look in two locations (ie ["*", "components/*"]
) for any module import in the project:
{ "compilerOptions": { "baseUrl": ".", "paths": { "*": ["*", "components/*"] } } }
In this example, the "*"
value in the array means the exact name of the module, while the "components/*"
value is the module name (“components) with an appended prefix.
We can now instruct the compiler to resolve the two imports as follows:
import 'view/file2'
The compiler will then substitute 'view/file2'
with the first location in the array ("*"
), and combine it with the baseUrl
which results in projectRoot/view/file2.ts
. This will allow the module to be found.
After this step, the compiler will move to the next import:
import 'nav/file3':
Likewise, the compiler will substitute 'nav/file3'
with the first location in the array ("*"
) — a.k.a. nav/file3
— and combine it with the baseUrl
which results in projectRoot/nav/file3.ts
. This time, the file does not exist, so the compiler will substitute 'nav/file3'
with the second location "components/*"
and combine it with the baseUrl
. This will result in projectRoot/components/nav/file3.ts
, thereby allowing the module to be found.
How TypeScript resolves modules by default
Without configuring the TypeScript compiler as discussed earlier, TypeScript will adopt the Node.js run-time resolution strategy by default in order to locate the requested modules at compile-time.
To accomplish this, the TypeScript compiler will look for .ts files, .d.ts, .tsx, and package.json files. If the compiler finds a package.json file, then it will check whether that file contains a types property that points to a typings file. If no such property is found, the compiler will then try looking for index files before moving on to the next folder.
Here’s an example. An import statement like import { b } from 'view/file2'
in /projectRoot/view/file1.ts
would result in attempting the following locations for locating ".view/file2"
:
- Does
/projectRoot/view/file2.ts
exist? - Does
/projectRoot/view/file2.tsx
exist? - Does
/projectRoot/view/file2.d.ts
exist? - Does
/projectRoot/view/file2/package.json
(if it specifies a"types"
property) exist? - Does
/projectRoot/view/file2/index.ts
exist? - Does
/projectRoot/view/file2/index.tsx
exist? - Does
/projectRoot/view/file2/index.d.ts
exist?
If the module is not found at this point, then the same process will be repeated, jumping a step out of the closest parent folder as follows:
- Does
/projectRoot/file2.ts
exist? - Does
/projectRoot/file2.tsx
exist? - Does
/projectRoot/file2.d.ts
exist? - Does
/projectRoot/file2/package.json
(if it specifies a"types"
property) exist? - Does
/projectRoot/file2/index.ts
exist? - Does
/projectRoot/file2/index.tsx
exist? - Does
/projectRoot/file2/index.d.ts
exist?
Conclusion
Sometimes, we may run into some complex situations where diagnosing why a module is not resolved can be so difficult. In such situations, enabling the compiler module resolution tracing using tsc --traceResolution
can provide more insight on what happened during the module resolution process, allowing us to choose the correct solution moving forward.
With the common TypeScript modules problems highlighted, and solutions provided in this post, I hope that it will become a bit easier to configure the TypeScript compiler to handle modules in your TypeScript projects.
Hopefully you’ve found this post informative and helpful. You can also check out the official TypeScript documentation for a deep dive into TypeScript module resolution.
LogRocket: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.
Try it for free.