Emmanuel John I'm a full-stack software developer, mentor, and writer. I am an open source enthusiast. In my spare time, I enjoy watching sci-fi movies and cheering for Arsenal FC.

Common TypeScript module problems and how to solve them

3 min read 1092

Typescript Mistakes

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:

We made a custom demo for .
No really. Click here to check it out.

// 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":

  1. Does /projectRoot/view/file2.ts exist?
  2. Does /projectRoot/view/file2.tsx exist?
  3. Does /projectRoot/view/file2.d.ts exist?
  4. Does /projectRoot/view/file2/package.json (if it specifies a "types" property) exist?
  5. Does /projectRoot/view/file2/index.ts exist?
  6. Does /projectRoot/view/file2/index.tsx exist?
  7. 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:

  1. Does /projectRoot/file2.ts exist?
  2. Does /projectRoot/file2.tsx exist?
  3. Does /projectRoot/file2.d.ts exist?
  4. Does /projectRoot/file2/package.json (if it specifies a "types" property) exist?
  5. Does /projectRoot/file2/index.ts exist?
  6. Does /projectRoot/file2/index.tsx exist?
  7. 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.

: Full visibility into your web 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 apps.

.
Emmanuel John I'm a full-stack software developer, mentor, and writer. I am an open source enthusiast. In my spare time, I enjoy watching sci-fi movies and cheering for Arsenal FC.

Leave a Reply