TypeScript · beginner

TypeScript cannot find module: separate types from runtime resolution

Debug TypeScript's "cannot find module" error (often TS2307) by separating the type-checker's resolution from runtime (Node, bundler) resolution. Inspect the active tsconfig, confirm package type metadata, and verify each change before touching paths or moduleResolution.

The symptoms

  • Editor displays red squiggles under an import path even though the package is installed in node_modules and the file runs at runtime.
  • CLI emits TS2307 "Cannot find module 'X' or its corresponding type declarations" during tsc --noEmit or a typed build.
  • The same import resolves successfully through the bundler or Node loader but the type checker rejects it, or the inverse.
  • Build only fails under one toolchain (for example tsc) while another (for example esbuild or vite) reports no error.
  • Adding a .js extension to a relative import silences the runtime error but produces a fresh TS error under module=commonjs.

Likely causes

  • compilerOptions.moduleResolution is set to a strategy (classic, node, node16, bundler) that does not match the package's published layout or the project's runtime loader.
  • The imported package declares no types entry in its package.json and no community @types/* package is installed alongside it.
  • baseUrl and paths entries point outside the tsconfig "include" scope, use the wrong root, or rely on a moduleResolution that ignores them.
  • Subpath imports rely on the package.json "exports" field, which classic and node moduleResolution do not fully understand (per the TypeScript moduleResolution reference).
  • The project uses ESM with module=node16 or nodenext but a relative import lacks the explicit .js extension TypeScript requires in that mode.
  • TypeScript is reading an unexpected tsconfig (a referenced sub-project, a workspace override, or the editor's bundled TS version) instead of the root tsconfig.json.

First ten minutes

  1. 01Capture the exact error code (TS2307 vs TS6053 vs TS2792 vs TS7016) and the import line that triggered it; record the tsconfig path reported by tsc.
  2. 02Confirm the failing file lives under the tsconfig "include" patterns by running tsc --showConfig and reading the effective compilerOptions.
  3. 03Open node_modules/<package>/package.json and check the types, typings, main, exports, and type fields for the module being imported.
  4. 04Compare the active moduleResolution against the project's runtime loader (Node ESM, Node CJS, Vite, Webpack) to find a resolver mismatch before editing anything.
  5. 05Run tsc --traceResolution for the failing import and isolate the first line that reports "Not found" or "Failed to resolve".

Evidence to collect

  • Full tsc output including file path, line, and column for the failing import, plus the error code so root cause can be classified.
  • The tsconfig.json actually in effect (tsc --showConfig), including compilerOptions.moduleResolution, module, baseUrl, paths, typeRoots, include, and exclude.
  • The imported package's package.json showing types, typings, exports, and main, and the on-disk file list under node_modules/<package>/ to confirm what was installed.
  • TypeScript version (tsc -v), Node version (node --version), and the build tool that surfaces the error (tsc, vite build, webpack, jest).

Where to look

  • The tsconfig.json that tsc and the editor resolve to — check for a references field, an outDir override, or a nested tsconfig next to the failing file.
  • node_modules/<package>/package.json, especially the types, typings, exports, and main fields; look for a /dist folder containing .d.ts files.
  • The node_modules/@types/ directory to verify whether community-maintained types are installed for the failing package.
  • The tsconfig "include" and "files" arrays to confirm the file containing the failing import is actually in scope.
  • Build-tool configuration (vite.config.ts, webpack.config.js, tsconfig "paths") where aliases may shadow real module names.

Diagnostic steps

  1. 01Reproduce with the smallest command: npx tsc --noEmit from the project root, then narrow to npx tsc --noEmit <file> to isolate which file actually fails.
  2. 02Run npx tsc --traceResolution and grep the failing module name; follow the chain until the "Not found" line and note which directory it last checked.
  3. 03Diff tsconfig.json compilerOptions against the runtime: if the project uses ESM, set moduleResolution to node16, nodenext, or bundler rather than leaving the legacy node value.
  4. 04Inspect node_modules/<package>/package.json: if exports is set and moduleResolution is node or classic, TypeScript cannot see subpaths — switch resolution or import a public entry instead.
  5. 05If types are missing on the package, verify availability with npm view @types/<package> name before installing, so you know whether the types are official.
  6. 06Validate baseUrl and paths resolve to real directories; remove trailing wildcards and keep the patterns as relative paths under baseUrl, not absolute URLs.

Common mistakes

  • Editing paths to alias a module without changing moduleResolution, leaving TypeScript unable to follow the alias under classic/node.
  • Adding .js to relative imports under module=commonjs, which TypeScript rejects; switch to a Node ESM strategy instead of dropping the extension.
  • Installing @types/* globally while the local TypeScript resolves only the local typeRoots, so the editor still cannot find the types.
  • Assuming tsc and the bundler share one resolver; they do not — Vite or esbuild may succeed where tsc fails, and the reverse is also common.
  • Treating TS2307 as a missing-file error and writing empty stub .d.ts files (declare module "x";) instead of fixing the real resolution path.

Safe fixes

  • Install the corresponding @types package with npm install --save-dev @types/<package>, then re-run tsc --noEmit to verify the error is gone before committing.
  • Update tsconfig.json compilerOptions.moduleResolution to match the runtime: bundler for Vite/Webpack projects, node16 or nodenext for ESM Node projects, as documented in the TypeScript moduleResolution reference.
  • Add a paths alias only with a matching baseUrl, and confirm the target directory is covered by include so the alias is actually visible to the checker.
  • Create a minimal ambient declaration (declare module "<name>";) only when the package genuinely ships no types and no @types package exists; document the limitation in a comment.
  • For ESM relative imports, keep the .js extension in source and set module plus moduleResolution to a Node ESM strategy so the extension is required, not forbidden.

Prove the fix

  1. 01npx tsc --noEmit exits 0 with the previously failing import still present in the source.
  2. 02Editor squiggles disappear after the TypeScript server reloads (or after restarting the editor process).
  3. 03The build pipeline that previously failed (tsc -b, vite build, or tsc followed by node dist/index.js) now completes end-to-end without a TS error for that import.
  4. 04tsc --traceResolution shows the package being resolved to a real .d.ts or .ts file rather than the previous "Not found" line.
  5. 05Re-running the original failing command on a clean checkout produces no TS error for that import, confirming the fix is reproducible.

Prevention and next steps

  • Pin module and moduleResolution in tsconfig.json and document which runtime they target; reject PRs that silently change either value.
  • Add a CI step that runs tsc --noEmit on a clean install so missing @types packages are caught before merge, not at deploy time.
  • Prefer bundler or node16 moduleResolution when adopting ESM exports fields, instead of leaving the legacy default in place.
  • Audit node_modules/<package>/package.json during dependency reviews to confirm new packages declare a types entry or are paired with @types.

Safe commands and checks

npx tsc --noEmit
npx tsc --showConfig
npx tsc --traceResolution > resolution.log
npx tsc -v
node --version
ls node_modules/<package>/
cat node_modules/<package>/package.json
npm view @types/<package> name