Skip to content

Fix: TypeScript Cannot Find Module or Its Corresponding Type Declarations

FixDevs ·

Quick Answer

How to fix the TypeScript error 'Cannot find module' or 'Could not find a declaration file for module' with proper type declarations and tsconfig settings.

The Error

You import a package or file in TypeScript and the compiler refuses to continue:

error TS2307: Cannot find module 'lodash' or its corresponding type declarations.

Or the less strict but equally annoying variant:

error TS7016: Could not find a declaration file for module 'some-library'.
'/home/user/project/node_modules/some-library/index.js' implicitly has an 'any' type.
  Try `npm i --save-dev @types/some-library` if it exists or add a new declaration (.d.ts)
  file containing `declare module 'some-library';`

You also see this when importing non-JavaScript files through a bundler:

error TS2307: Cannot find module './styles.css' or its corresponding type declarations.
error TS2307: Cannot find module './logo.png' or its corresponding type declarations.

Or when using path aliases:

error TS2307: Cannot find module '@/components/Button' or its corresponding type declarations.

All of these mean the same thing: TypeScript knows you are trying to import something, but it cannot locate type information for it. Without type information, it either blocks compilation entirely (TS2307) or flags the import as any (TS7016).

Why This Happens

TypeScript resolves modules in two phases. First, it locates the JavaScript code (or confirms the module exists). Second, it looks for type information describing the module’s exports. The error fires when the second step fails.

Type information can come from three places:

  1. Bundled types. The package ships its own .d.ts files. Check for a types or typings field in the package’s package.json. Libraries like axios, zod, and date-fns do this.
  2. DefinitelyTyped. Community-maintained type definitions published under the @types/ scope on npm. Covers packages that do not ship their own types, like express, lodash, and react.
  3. Your own declaration files. A .d.ts file in your project where you manually describe the module’s shape.

When none of these exist for a given import, TypeScript throws an error. The specific cause usually falls into one of these categories:

  • The package has no bundled types and you have not installed its @types/ counterpart.
  • Your tsconfig.json settings are filtering out type definitions that exist on disk.
  • You are importing a non-JavaScript file (CSS, images, JSON) that TypeScript does not understand natively.
  • Your moduleResolution setting does not match how your runtime or bundler actually resolves modules.
  • Your baseUrl or paths configuration is incorrect or incomplete.
  • Your typeRoots or types settings are overriding default behavior and excluding packages you need.

Fix: Install the Missing @types/ Package

The most common cause is a missing DefinitelyTyped package. Many popular npm packages do not ship their own types, so the community maintains them under the @types/ scope.

Install the corresponding @types/ package as a dev dependency:

npm install --save-dev @types/lodash

For Node.js built-in modules and globals like process, Buffer, __dirname, and require:

npm install --save-dev @types/node

You can install multiple type packages at once:

npm install --save-dev @types/express @types/cors @types/cookie-parser

To check whether an @types/ package exists for a given library:

npm search @types/some-library

Not every package needs a separate @types/ install. Some ship types directly. If a package’s package.json has a types or typings field pointing to a .d.ts file, its types are already bundled and no extra install is needed.

If you run into dependency conflicts while installing @types/ packages, see Fix: npm ERR! ERESOLVE unable to resolve dependency tree for resolution strategies.

Fix: Create a declare module Shim

When no @types/ package exists and the library does not ship types, you can write your own declaration. This is common for small or niche JavaScript libraries that have no community type coverage.

Quick shim: treat everything as any

Create a .d.ts file anywhere TypeScript can see it (typically in your src/ directory or a src/types/ folder):

// src/types/some-library.d.ts
declare module 'some-library';

This tells TypeScript the module exists but provides no type information. All imports from it resolve to any. You lose type safety for that module, but the error goes away immediately.

Typed shim: declare the exports you use

If you know the library’s API, provide specific type declarations:

// src/types/some-library.d.ts
declare module 'some-library' {
  export function parse(input: string): Record<string, unknown>;
  export function stringify(data: unknown): string;
  export default class Client {
    constructor(options: { apiKey: string; timeout?: number });
    request(endpoint: string): Promise<unknown>;
  }
}

This gives you autocompletion and type checking for the exports you declared while keeping the rest uncovered.

Wildcard module declarations

You can use wildcard patterns to cover an entire class of imports at once. This is especially useful for non-JavaScript file types:

// src/types/modules.d.ts
declare module '*.css';
declare module '*.scss';
declare module '*.svg';
declare module '*.png';
declare module '*.jpg';
declare module '*.json';

A bare declare module '*.css' treats the default export as any. For more precise typing, define the export shape:

declare module '*.css' {
  const classes: Record<string, string>;
  export default classes;
}

declare module '*.svg' {
  const src: string;
  export default src;
}

Where .d.ts files must live

Your declaration file must be in a location matched by the include pattern in tsconfig.json. If your config says "include": ["src/**/*"], a .d.ts file in the project root will be invisible to TypeScript. Place it inside src/ or add its path to include:

{
  "include": ["src/**/*", "custom-types.d.ts"]
}

Common conventions are src/types/, src/@types/, or a top-level typings/ directory. Pick one and be consistent.

Fix: CSS and Image Module Declarations

Bundlers like Webpack and Vite allow importing CSS, images, and other assets directly in JavaScript. TypeScript does not understand these imports out of the box:

error TS2307: Cannot find module './App.module.css' or its corresponding type declarations.
error TS2307: Cannot find module './logo.svg' or its corresponding type declarations.

Create a declaration file that tells TypeScript what these imports resolve to:

// src/types/assets.d.ts

// CSS Modules
declare module '*.module.css' {
  const classes: Record<string, string>;
  export default classes;
}

// Plain CSS (side-effect imports like `import './global.css'`)
declare module '*.css' {
  const content: string;
  export default content;
}

// SCSS Modules
declare module '*.module.scss' {
  const classes: Record<string, string>;
  export default classes;
}

// Images
declare module '*.png' {
  const src: string;
  export default src;
}

declare module '*.jpg' {
  const src: string;
  export default src;
}

declare module '*.jpeg' {
  const src: string;
  export default src;
}

declare module '*.gif' {
  const src: string;
  export default src;
}

declare module '*.webp' {
  const src: string;
  export default src;
}

// SVG (if imported as a URL string)
declare module '*.svg' {
  const src: string;
  export default src;
}

Vite users: Vite ships built-in type definitions for common asset types. Add a reference to vite/client in your tsconfig.json:

{
  "compilerOptions": {
    "types": ["vite/client"]
  }
}

Or add a triple-slash directive in a .d.ts file:

/// <reference types="vite/client" />

This covers CSS, images, JSON, and other asset imports. You typically do not need a custom asset declaration file when using Vite. If you are still seeing import resolution errors specific to Vite, check out the Vite import resolution guide for additional fixes.

Fix: Set the Right moduleResolution

The moduleResolution setting in tsconfig.json controls how TypeScript locates modules. Using the wrong value is one of the most common causes of “cannot find module” errors, because TypeScript’s resolution algorithm diverges from what your runtime or bundler actually does.

{
  "compilerOptions": {
    "moduleResolution": "bundler"
  }
}

Here is when to use each value:

ValueUse when
"node"Legacy CommonJS Node.js projects (no ESM, no "type": "module" in package.json)
"node16" / "nodenext"Node.js projects using ESM, or hybrid CJS/ESM packages
"bundler"Projects using Webpack, Vite, esbuild, Turbopack, or any other bundler

Why "node" causes problems

The "node" value uses the legacy CommonJS resolution algorithm. It does not understand:

  • The exports field in package.json (subpath exports, conditional exports)
  • File extension enforcement required by ESM
  • The imports field for self-referencing packages

If a package uses exports to define its entry points, TypeScript with "moduleResolution": "node" will not find them. Switching to "bundler" or "nodenext" fixes this immediately.

"nodenext" vs "bundler"

Use "nodenext" when you are running code directly in Node.js without a bundler. It enforces strict ESM rules: file extensions are mandatory in relative imports, and require() of ESM-only packages is forbidden.

Use "bundler" when a bundler handles your imports. Bundlers are more lenient: they resolve extensions automatically, allow mixing ESM and CJS freely, and support non-standard import patterns like CSS imports. Most frontend frameworks (Next.js, Vite, Remix) should use "bundler".

ESM vs CJS interop pitfalls

When you mix ESM and CommonJS, the way default exports work changes. A CommonJS module that does module.exports = something does not have a real default export in ESM terms. TypeScript’s handling of this differs based on your module and moduleResolution settings.

If you are importing a CommonJS package in an ESM project and getting errors, try enabling esModuleInterop:

{
  "compilerOptions": {
    "esModuleInterop": true,
    "moduleResolution": "nodenext",
    "module": "nodenext"
  }
}

With esModuleInterop enabled, TypeScript generates helper code that lets you write import express from 'express' even when express uses module.exports. Without it, you need to write import * as express from 'express' or import express = require('express').

If you are dealing with runtime MODULE_NOT_FOUND errors rather than TypeScript compile-time errors, see Fix: Error Cannot find module in Node.js for Node.js-specific resolution.

Real-world scenario: Switching moduleResolution from "node" to "bundler" is one of the most impactful single-line fixes in TypeScript configuration. Many “cannot find module” errors instantly disappear because "bundler" mode understands package.json exports fields that "node" mode ignores entirely.

Fix: Configure paths and baseUrl

Path aliases like @/components/Button are popular in modern projects. TypeScript supports them through the paths and baseUrl options in tsconfig.json, but misconfiguration causes “cannot find module” errors.

Setting up paths

{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@utils/*": ["src/utils/*"]
    }
  }
}

With this config, import Button from '@/components/Button' resolves to src/components/Button during type checking.

Common mistakes

Missing baseUrl: The paths option is relative to baseUrl. If you omit baseUrl, TypeScript uses the location of tsconfig.json as the base, which may not be what you expect. Always set baseUrl explicitly when using paths.

Paths only affect type checking: TypeScript’s paths option tells the compiler where to find types. It does not rewrite the import paths in the emitted JavaScript. Your runtime or bundler also needs to know about these aliases:

  • Next.js reads tsconfig.json paths automatically.
  • Vite needs vite-tsconfig-paths plugin or manual resolve.alias configuration.
  • Webpack needs tsconfig-paths-webpack-plugin or manual resolve.alias.
  • ts-node / tsx needs the tsconfig-paths package:
    npm install --save-dev tsconfig-paths
    node -r tsconfig-paths/register -r ts-node/register src/index.ts
  • Plain tsc output needs tsc-alias to rewrite paths after compilation:
    npm install --save-dev tsc-alias
    tsc && tsc-alias

If both TypeScript and your bundler are not configured consistently, you will get errors in one tool but not the other.

Fix: Configure typeRoots and types

By default, TypeScript automatically discovers and loads type definitions from every @types/ package in node_modules/@types/. The typeRoots and types options in tsconfig.json override this behavior, and misconfiguring them is a common source of missing module errors.

typeRoots

Specifies which directories TypeScript searches for type packages:

{
  "compilerOptions": {
    "typeRoots": ["./node_modules/@types", "./src/types"]
  }
}

If you set typeRoots to only a custom directory, TypeScript stops looking at node_modules/@types. Always include "./node_modules/@types" alongside your custom type root.

types

Specifies which @types/ packages to include. When set, TypeScript only loads the packages you list:

{
  "compilerOptions": {
    "types": ["node", "jest"]
  }
}

With this config, @types/express is ignored even if it is installed. This is a frequent gotcha: you install @types/express, but TypeScript still complains because the types array does not include "express".

Best practice: If you do not have a specific reason to restrict which types are loaded, remove both typeRoots and types entirely. Let TypeScript use its default behavior of loading everything from node_modules/@types.

If you set types to include framework-specific type packages (like "vite/client" or "@testing-library/jest-dom"), remember to add every @types/ package you depend on as well:

{
  "compilerOptions": {
    "types": ["node", "jest", "vite/client"]
  }
}

Fix: Writing Proper .d.ts Files

Declaration files (.d.ts) are TypeScript’s mechanism for describing JavaScript code that has no inline type annotations. Understanding how they work prevents a class of subtle errors.

Ambient vs. module declarations

A .d.ts file without any import or export statements at the top level is treated as an ambient (script) file. Declarations in it are automatically global:

// global.d.ts (no import/export at top level)
declare module 'untyped-lib';          // ambient module declaration
declare const API_URL: string;          // global constant

A .d.ts file with a top-level import or export is treated as a module. Declarations in it are scoped to that module unless you use declare global:

// env.d.ts (has export, so it's a module)
declare global {
  namespace NodeJS {
    interface ProcessEnv {
      DATABASE_URL: string;
      NODE_ENV: 'development' | 'production' | 'test';
    }
  }
}

export {};

The export {} at the bottom is critical. Without it, the file has no top-level exports and TypeScript treats it as a script, which changes how declare global merges into the global scope. Always include export {} in .d.ts files that use declare global.

Common .d.ts mistakes

Using .ts instead of .d.ts: A regular .ts file with declare module works differently from a .d.ts file. The .ts file is compiled and emitted; the .d.ts file is purely a type definition that is never emitted. For ambient module declarations, always use .d.ts.

Putting the file outside include: If your tsconfig.json has "include": ["src/**/*"] and your .d.ts file is in the project root, TypeScript will not see it. Move it inside src/ or update include.

Forgetting to restart the TypeScript server: VS Code caches type information. After creating or modifying a .d.ts file, open the command palette (Ctrl+Shift+P / Cmd+Shift+P) and run “TypeScript: Restart TS Server” to force a reload.

Fix: ESM vs CJS Module Interop

Module format mismatches between your project and the packages you import are a growing source of “cannot find module” errors, especially as the ecosystem transitions from CommonJS to ESM.

Importing an ESM-only package from CJS

Some modern packages ship only ESM (no CommonJS fallback). If your project uses CommonJS ("type": "module" is absent from package.json) and you try to require() an ESM-only package, Node.js throws an error. TypeScript may also fail to resolve the module’s types.

Options:

  1. Switch your project to ESM. Add "type": "module" to your package.json and update tsconfig.json:

    {
      "compilerOptions": {
        "module": "nodenext",
        "moduleResolution": "nodenext"
      }
    }
  2. Use dynamic import(). In CommonJS, you can use await import('esm-only-package') to load ESM packages dynamically. TypeScript understands this pattern.

  3. Pin an older version. Some packages have older versions that still ship CJS. Check the package’s changelog for when they dropped CJS support.

Importing a CJS package from ESM

When importing a CommonJS package in an ESM project, default imports may not work as expected. A CJS module that does module.exports = { foo, bar } does not have a true default export:

// May fail depending on the package
import lib from 'cjs-package';

// More reliable -- use named imports
import { foo, bar } from 'cjs-package';

// Or import the namespace
import * as lib from 'cjs-package';

Enable esModuleInterop in tsconfig.json to smooth over these differences:

{
  "compilerOptions": {
    "esModuleInterop": true
  }
}

If you are seeing a related error where ESLint cannot parse your TypeScript files because of module syntax issues, see Fix: ESLint Parsing error: Unexpected token for ESLint configuration adjustments.

Fix: Handle Multiple tsconfig Files

Large projects often have multiple TypeScript configurations. A Vite project might have tsconfig.json, tsconfig.app.json, and tsconfig.node.json. A Next.js monorepo might have a separate config per package.

The problem

Each tsconfig file has its own include, types, typeRoots, and paths settings. Type definitions available in one config are not automatically available in another. If you add @types/node to tsconfig.node.json but your source files are governed by tsconfig.app.json, those source files still cannot see Node.js types.

The fix

Check which tsconfig applies to the file throwing the error. In VS Code, open a .ts file and look at the status bar at the bottom. It shows the TypeScript version and which config is active. Click it to verify.

Make sure each config has the correct settings:

// tsconfig.app.json -- for src/ files
{
  "extends": "./tsconfig.json",
  "include": ["src/**/*", "src/**/*.d.ts"],
  "compilerOptions": {
    "types": ["vite/client"]
  }
}
// tsconfig.node.json -- for config files like vite.config.ts
{
  "extends": "./tsconfig.json",
  "include": ["vite.config.ts"],
  "compilerOptions": {
    "types": ["node"]
  }
}

Settings from extends are inherited, but include, exclude, types, and typeRoots defined in the child config fully replace (not merge with) the parent values.

Still Not Working?

Restart the TypeScript server

VS Code runs its own TypeScript language server that caches type information aggressively. After installing @types/ packages, creating .d.ts files, or changing tsconfig.json, the cached state can be stale.

Open the command palette (Ctrl+Shift+P / Cmd+Shift+P) and run “TypeScript: Restart TS Server”. This forces VS Code to reload all type definitions from scratch.

Delete build caches

If you use "incremental": true in tsconfig.json, TypeScript caches build information in a .tsbuildinfo file. Stale caches cause phantom errors:

rm -f tsconfig.tsbuildinfo
npx tsc --noEmit

Verify include covers your files

Run tsc --listFiles to see every file TypeScript is processing:

npx tsc --listFiles | grep -i "your-module"

If your .d.ts file does not appear in the output, it is not covered by include. Update tsconfig.json to include it.

Check for duplicate type versions

If you see a type error where the types look identical but TypeScript says they are incompatible, you may have duplicate versions of the same @types/ package. This is common in monorepos or projects with nested dependencies. For more on this specific scenario, see Fix: Type ‘X’ is not assignable to type ‘Y’.

Check for duplicates:

npm ls @types/react
npm ls @types/node

Deduplicate with:

npm dedupe

Or force a single version with overrides in package.json:

{
  "overrides": {
    "@types/react": "^18.2.0"
  }
}

Use skipLibCheck as a last resort

If the errors are coming from inside node_modules or from .d.ts files you did not write, you can skip type checking on all declaration files:

{
  "compilerOptions": {
    "skipLibCheck": true
  }
}

This is a recommended default in most framework starter templates. It speeds up compilation and avoids errors caused by conflicting type definitions between packages. It does not affect type checking on your own source files.

Check the useEffect pattern for React projects

If you landed here while debugging a React project and the “cannot find module” error is a secondary symptom of a component that re-renders infinitely and loses track of its module state, the root cause might be an infinite loop in useEffect rather than a module resolution problem.


Related: Fix: Error Cannot find module in Node.js | Fix: Type ‘X’ is not assignable to type ‘Y’ | Fix: ESLint Parsing error: Unexpected token

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles