VOOZH about

URL: https://blog.logrocket.com/common-typescript-module-problems-how-to-solve/

โ‡ฑ Common TypeScript module problems and how to solve them - LogRocket Blog


2023-09-07
1812
#typescript
Emmanuel John
33624
๐Ÿ‘ Image

See how LogRocket's Galileo AI surfaces the most severe issues for you

No signup required

Check it out

Editorโ€™s note: This article was last updated on 7 September 2023 to include information about the โ€œambiguous module declarationsโ€ TypeScript error.

๐Ÿ‘ Common TypeScript Module Problems And How To Solve Them

Build processes in TypeScript can be difficult to achieve, especially when manually configuring a project through the  tsconfig.json file. This is often because these configurations require understanding the TypeScript compiler and module system.

Drawing from my experience with TypeScript projects, Iโ€™ve identified the most common issues associated with TypeScript modules and, more importantly, how to resolve them effectively. In this article, weโ€™ll cover the following problems and their solutions:

๐Ÿš€ Sign up for The Replay newsletter

The Replay is a weekly newsletter for dev and engineering leaders.

Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.

Prerequisites

To follow along with this article, it is recommended that you have:

  • A strong background in JavaScript and TypeScript
  • A firm understanding of the 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

However, sometimes modules are not directly located under 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 runtime.

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, the TypeScript compiler will throw the following error:
express module not found.

Hereโ€™s whatโ€™s happening under the hood: The 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: Locating the correct directory

The mapping in "paths" is resolved relative to "baseUrl". Therefore, 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, the 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, the TypeScript compiler will search for the node_modules directory in the root directory of the project. It is important to maintain a structured project directory as this makes it easy for the compiler to get the required file/module it needs. A structured directory also makes locating necessary files easier for engineers.

Problem 2: Multiple fallback locations

Our second common TypeScript module 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 is in the components directory. Resolving modules from multiple locations can be challenging because, at this point in your code, the TypeScript compiler lacks the necessary context to effectively determine how to locate and use these modules from different parts of your project. In the following section, we will review how to solve this issue.

Solution 2: Locating the module and resolve imports

Using the configuration below, we can tell the compiler to look in two locations (i.e., ["*", "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.

Now, we can 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 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 ("*") โ€” i.e., nav/file3 โ€” and combine it with the baseUrl. This 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 baseUrl. This will result in projectRoot/components/nav/file3.ts, thereby allowing the module to be found.

Though we just showed how TypeScript locates the file, it is still important to structure your files in a way that is easily accessible. Organizing files in structured folders has many advantages:

  • Organization/maintenance: A well-structured directory makes it easier to find files and navigate through your project. This is particularly helpful to both the compiler and engineer as the project grows, and it is quicker to locate and modify specific components or modules
  • Testing: A clear directory structure facilitates unit testing and debugging by ensuring that files are logically organized
  • Easier readability/collaboration: A well-organized directory makes the project more readable and understandable for both you and your collaborators, as it easily conveys the project architecture and logic. Working in teams becomes seamless as it reduces confusion and promotes a common understanding of where to find and place code

The rootDirs property in your tsconfig.json file allows the merging of multiple directories into a single logical directory for compilation.

Consider an example where two files are stored in different directories: src/views/view1.ts and generated/templates/views/template1.ts. Imagine that view1.ts tries to do a relative import of template1.ts like so:

//view1.ts
import './template1'

This import would not be resolved and instead, would give an error because template1.ts doesnโ€™t exist in the same folder. To solve this issue, add both folders to the rootDirs property in the tsconfig.json file:

"compilerOptions": {
 "rootDirs": ["src/views", "generated/templates/views"]
 }

This creates a virtual directory, which enables the import of the .template1.ts to work at runtime. view1.ts now knows template1.ts exists next to it, and is able to import .template1.ts using a relative name like "./template".

This is made possible using the rootDirs property.

Problem 3: Ambiguous module declarations

The problem of ambiguous module declarations occurs when there are conflicting or ambiguous module declarations in the codebase. Suppose you have three TypeScript files in your project: log.ts and log.d.ts and test.ts.

This is what the log.ts file looks like:

// log.ts
export const show = (val: string) => val;

Here is the log.d.ts file:

// log.d.ts
declare module 'log' {
 export function show(val: string): string;
}

Finally, hereโ€™s the test.ts file:

import * as log from 'log';

const result = log.show("test");
console.log(result);

An error message โ€œAmbiguous module declarationsโ€ would be thrown because TypeScript canโ€™t determine which declaration to use for the 'log' module: the one from log.ts or the one from log.d.ts.

Solution 3: Avoiding module conflict

To help address this problem, avoid global/module conflict in declarations. Limit the use of global declarations to only whatโ€™s necessary, as overusing global declarations can lead to conflicts. Another fix would be to organize your declaration files into a clear folder structure, using meaningful names for declaration files to to avoid naming conflicts.


Over 200k developers use LogRocket to create better digital experiences

๐Ÿ‘ Image
Learn more โ†’

How TypeScript handles module resolution

Module resolution is the method employed by the TypeScript compiler to determine the target of an import statement. It accomplishes this by following a configurable strategy capable of adapting to various module systems and directory layouts. The following is how TypeScript resolves modules:

  1. Module resolution algorithm: Typescript checks if the import path is relative or non-relative. If relative, it locates the module based on the fileโ€™s physical location. If itโ€™s non-relative, it uses configured module resolution settings for that module
  2. Configured module resolution: TypeScript relies on a tsconfig.json file to configure module resolution settings, as we saw in Solution 2 above. The baseUrl setting specifies the base directory used to resolve non-relative module paths
  3. Declaration files: Also, it can use declaration files (.d.ts) to provide type information for the JavaScript module

TypeScript resolving 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, .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 finding ".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 complex situations where it can be difficult to diagnose why a module is not resolved. In these situations, enabling compiler module resolution tracing using tsc --traceResolution can provide insight into what happened during the module resolution process, allowing us to choose the correct solution.

With the common TypeScript module problems and their solutions highlighted in this post, I hope that it will become easier to configure the TypeScript compiler to handle modules in your TypeScript projects.

I hope you found this post informative and helpful. You can also check out the official TypeScript documentation for a deep dive into TypeScript module resolution.

LogRocket understands everything users do in your web and mobile apps.

๐Ÿ‘ LogRocket Dashboard Free Trial Banner

LogRocket lets you replay user sessions, eliminating guesswork by showing exactly what users experienced. It captures console logs, errors, network requests, and pixel-perfect DOM recordings โ€” compatible with all frameworks, and with plugins to log additional context from Redux, Vuex, and @ngrx/store.

With Galileo AI, you can instantly identify and explain user struggles with automated monitoring of your entire product experience.

Modernize how you understand your web and mobile apps โ€” start monitoring for free.

๐Ÿ‘ Image
๐Ÿ‘ Image
๐Ÿ‘ Image

Stop guessing about your digital experience with LogRocket

Get started for free

Recent posts:

Debug Next.js apps with AI agents and next-browser

Learn how next-browser gives AI agents runtime context for debugging Next.js apps, including React props, hydration, PPR, forms, and performance.

๐Ÿ‘ Image
Emmanuel John
Jun 17, 2026 โ‹… 9 min read

Stop hardcoding LLM SDKs: Dynamic LLM routing with OpenRouter and Next.js

Build dynamic LLM routing in Next.js with OpenRouter, TanStack AI, task classification, model fallbacks, and cost-aware routing.

๐Ÿ‘ Image
Chizaram Ken
Jun 16, 2026 โ‹… 13 min read

What is TSRX?: What JSX would look like if it were designed today

TSRX adds first-class control flow, conditional hooks, and scoped styles to React via a TypeScript compiler extension โ€” no new framework required.

๐Ÿ‘ Image
Ikeh Akinyemi
Jun 12, 2026 โ‹… 6 min read

How to add authentication to a React Native app with Better Auth

Learn how to build a full React Native auth system using Better Auth and Expo โ€” with email/password login, Google OAuth, session persistence, and protected routes.

๐Ÿ‘ Image
Chinwike Maduabuchi
Jun 9, 2026 โ‹… 13 min read
View all posts

Would you be interested in joining LogRocket's developer community?

Join LogRocketโ€™s Content Advisory Board. Youโ€™ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.

Sign up now