Ch. 2 · TypeScript

TS2307: Cannot Find Module or Its Type Declarations in TypeScript

Fix TS2307 'Cannot find module or its corresponding type declarations' and TS7016 by tracing module resolution, @types, paths and exports.

~7 min readintermediateupdated Oct 4, 2026

An import that works at runtime, or worked yesterday, now fails to type-check:

src/main.ts(3,24): error TS2307: Cannot find module '@acme/ui' or its corresponding type declarations.
src/main.ts(1,17): error TS7016: Could not find a declaration file for module 'legacy-pad'. '/path/to/app/node_modules/legacy-pad/index.js' implicitly has an 'any' type.
  Try `npm i --save-dev @types/legacy-pad` if it exists or add a new declaration (.d.ts) file containing `declare module 'legacy-pad';`
Text

(Absolute paths are shortened to /path/to/app here.) These are two different outcomes of module resolution. TS2307 means TypeScript looked for the module specifier and found nothing it could read: no .ts, no .d.ts, no package types. TS7016 means it found the package’s JavaScript file but no types for it; with noImplicitAny (part of strict) that is an error, without it the import silently becomes any.

Quick fix checklist

  • Is it installed in this workspace? Run npm ls <name>; in a monorepo, check the package that contains the importing file.
  • TS7016 for a third-party package: npm i -D @types/<name>. If no such package exists, write a declare module "<name>" block.
  • TS7016 with “There are types at … when respecting package.json “exports”“: the package’s exports map hides its own types; upgrade it or declare the module yourself.
  • Importing .svg, .png or .css: add your bundler’s client types (for Vite, "types": ["vite/client"]) or a wildcard declare module "*.svg".
  • An alias like @/utils: add paths to tsconfig.json with ./-prefixed targets, and configure the same alias in the bundler.
  • node:fs and other built-ins on TypeScript 6.0 or later: install @types/node and add "types": ["node"].
  • Run npx tsc --traceResolution when none of the above explains it.

Before you start

Know which compiler and settings you actually have: npx tsc -v and npx tsc --showConfig (which prints the resolved config including anything inherited through extends). TypeScript 6.0 changed several defaults that surface as resolution errors after an upgrade:

  • types now defaults to [], so @types/* packages are no longer loaded automatically. A Node built-in import then fails with TS2591 (Cannot find name 'node:fs/promises'. Do you need to install type definitions for node?) even though @types/node is installed.
  • noUncheckedSideEffectImports is on, so import "./styles.css" reports TS2882 (Cannot find module or type declarations for side-effect import of './styles.css').
  • moduleResolution: "node" (also called node10) and baseUrl are deprecated. In 7.0 they are removed and the config itself fails with TS5108 and TS5102.

Messages below were produced by TypeScript 7.0; 5.9 prints identical TS2307 and TS7016 text.

Why it happens

For every import specifier, TypeScript runs a resolution algorithm chosen by moduleResolution and looks for a file it can read types from:

  1. Relative specifiers (./math) resolve against the importing file. Under node16 and nodenext, ES module files must spell the extension the way the output will (./math.js), because Node requires it at runtime. Leaving it out gives TS2835.
  2. Bare specifiers (legacy-pad) are looked up in node_modules. TypeScript reads the package’s package.json: if it has an exports map and the mode supports it (bundler, node16, nodenext), only exports counts. It picks the matching condition (types, import, require, default) and looks for a declaration file next to the resolved JavaScript. Without exports, it uses types/typings, then main.
  3. If the package has no types, TypeScript tries node_modules/@types/<name>.
  4. paths in tsconfig.json rewrites matching specifiers before this search. It only affects type checking; it never changes the emitted import, so your bundler or runtime needs the same alias.
  5. Ambient declarations (declare module "x") in included .d.ts files win when nothing else is found.

Assets are a special case. TypeScript has no idea what import logo from "./logo.svg" produces; your bundler decides that at build time, so you must describe it with a declaration.

Step-by-step walkthrough

Step 1: Reproduce and classify every error

Run the compiler over the whole project:

npx tsc --noEmit --pretty false
Terminal
src/main.ts(1,17): error TS7016: Could not find a declaration file for module 'legacy-pad'. ...
src/main.ts(2,22): error TS7016: Could not find a declaration file for module 'color-kit'. '/path/to/app/node_modules/color-kit/dist/index.js' implicitly has an 'any' type.
  There are types at '/path/to/app/node_modules/color-kit/types/index.d.ts', but this result could not be resolved when respecting package.json "exports". The 'color-kit' library may need to update its package.json or typings.
src/main.ts(3,24): error TS2307: Cannot find module '@acme/ui' or its corresponding type declarations.
src/main.ts(4,18): error TS2307: Cannot find module './logo.svg' or its corresponding type declarations.
src/main.ts(5,28): error TS2307: Cannot find module '@/utils/date' or its corresponding type declarations.
Text

Each line has a different cause: an untyped package, a package whose exports hide its types, a missing install, an asset and an alias. The same project also gets the TS2591 and TS2882 lines from the previous section for its node:fs/promises and ./styles.css imports.

Step 2: Trace resolution for the confusing ones

npx tsc --noEmit --traceResolution > trace.txt
grep -n "Resolving module 'color-kit'" trace.txt
Terminal

The trace for color-kit contains these lines (paths shortened):

Matched 'exports' condition 'import'.
Using 'exports' subpath '.' with target './dist/index.js'.
File '/path/to/app/node_modules/color-kit/dist/index.d.ts' does not exist.
Failed to resolve under condition 'import'.
Text

The package’s package.json has "types": "./types/index.d.ts" at the top level but an exports map with only an import condition. Because exports wins, TypeScript never reads the top-level types. The real fix belongs in the package (an exports entry with a types condition); until then you can declare the module locally.

Step 3: Provide types for packages

npm ls @acme/ui            # empty: not installed in this workspace
npm i @acme/ui
npm i -D @types/legacy-pad # only if it exists on npm
Terminal

When there is no @types package, describe the part of the API you use in a file such as src/declarations.d.ts:

declare module "legacy-pad" {
  export default function pad(input: string, length: number, fill?: string): string;
}

declare module "color-kit" {
  export function tint(color: string): string;
}
TypeScript

The shorthand declare module "legacy-pad"; also silences TS7016, but every import from it becomes any. Write the signatures you call; it takes minutes and catches misuse.

Step 4: Describe assets and aliases

declare module "*.svg" {
  const url: string;
  export default url;
}

declare module "*.css";
TypeScript

Match what your bundler returns: Vite gives a URL string for .svg by default, an SVGR setup gives a React component. With Vite, "types": ["vite/client"] provides these declarations.

For aliases, use ./-prefixed targets without baseUrl:

{
  "compilerOptions": {
    "moduleResolution": "bundler",
    "types": ["node"],
    "paths": { "@/*": ["./src/*"] }
  }
}
JSON

Step 5: Choose the right moduleResolution

Use bundler when Vite, webpack, esbuild or a framework compiles your imports: it understands exports and needs no file extensions. Use nodenext when Node runs the emitted JavaScript directly: it enforces the same rules Node will (extensions, ESM versus CommonJS per package.json type). Choosing bundler for a Node service hides errors that only appear at runtime as ERR_MODULE_NOT_FOUND.

Worked scenario

A Node API is upgraded to TypeScript 7.0. Its old config:

{
  "compilerOptions": {
    "strict": true,
    "module": "commonjs",
    "moduleResolution": "node",
    "baseUrl": ".",
    "paths": { "@/*": ["src/*"] },
    "outDir": "dist"
  },
  "include": ["src"]
}
JSON
tsconfig.json(5,25): error TS5108: Option 'moduleResolution=node10' has been removed. Please remove it from your configuration.
tsconfig.json(6,5): error TS5102: Option 'baseUrl' has been removed. Please remove it from your configuration.
  Use '"paths": {"*": ["./*"]}' instead.
tsconfig.json(7,24): error TS5090: Non-relative paths are not allowed. Did you forget a leading './'?
Text

Switching to nodenext without other changes surfaces the next layer:

src/server.ts(2,28): error TS2307: Cannot find module '@/utils/date' or its corresponding type declarations.
src/server.ts(3,21): error TS2835: Relative import paths need explicit file extensions in ECMAScript imports when '--moduleResolution' is 'node16' or 'nodenext'. Did you mean './math.js'?
Text

Diagnosis: paths was never seen by Node at runtime (a separate loader rewrote it), and the extensionless import only worked under the old algorithm. The fix uses package.json subpath imports, which both TypeScript and Node understand:

{
  "name": "orders-api",
  "type": "module",
  "imports": { "#utils/*": "./dist/utils/*" }
}
JSON
{
  "compilerOptions": {
    "strict": true,
    "module": "nodenext",
    "moduleResolution": "nodenext",
    "types": ["node"],
    "rootDir": "./src",
    "outDir": "dist"
  },
  "include": ["src"]
}
JSON
import { readFile } from "node:fs/promises";
import { formatDate } from "#utils/date.js";
import { sum } from "./math.js";
TypeScript

TypeScript maps ./dist/utils/date.js back to src/utils/date.ts through rootDir and outDir, and Node resolves the same specifier in the compiled output.

Common mistake

Adding // @ts-ignore above the import, or declare module "*"; to make every unknown module any. Both hide real problems: a typo in a package name, a dependency missing from package.json that only happens to exist through hoisting, or a wrong moduleResolution that will break at runtime.

Another is fixing paths and assuming the build works. paths does not rewrite output, so tsc passes and Node then throws ERR_MODULE_NOT_FOUND.

Verify the behavior

Check both halves. npx tsc --noEmit should exit with status 0. Then prove the runtime agrees: for a Node service, build and import the output (npx tsc && node -e 'import("./dist/server.js")'); for a bundled app, run the production build. To see what a specifier resolved to, grep the summary line of the trace:

npx tsc --noEmit --traceResolution | grep "Module name 'color-kit'"
Terminal
======== Module name 'color-kit' was successfully resolved to '/path/to/app/node_modules/color-kit/dist/index.js' with Package ID 'color-kit/dist/index.js@2.0.0'. ========
Text

“Successfully resolved” to a .js file is exactly the TS7016 situation: TypeScript found code, not types. Once the library ships a types condition, this line points at a .d.ts. With the local workaround it still points at the .js and the types come from your declare module block. A specifier that prints was not resolved (like @acme/ui before installing it) is the TS2307 case.

Interview exercise

A library’s package.json has "types": "./types/index.d.ts" and "exports": { ".": { "import": "./dist/index.js" } }. Consumers with moduleResolution: "node10" get types, but consumers on bundler or nodenext get TS7016. Why, and how should the library fix it?

Answer and reasoning

node10 ignores exports and reads the top-level types field. bundler, node16 and nodenext follow exports strictly, as Node does at runtime: they match the import condition, resolve ./dist/index.js, and look for ./dist/index.d.ts beside it. It does not exist, and the top-level types field is not consulted once exports is present, so the package is effectively untyped.

The library should add a types condition first in each exports entry, for example { "types": "./types/index.d.ts", "import": "./dist/index.js" }, or emit declarations next to the JavaScript. Condition order matters because the first match wins. If it ships both ESM and CommonJS, each format needs its own declaration file (.d.mts and .d.cts) so types describe the format actually loaded.

Continue learning

More in TypeScript

esc