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
- 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.
- 02Confirm the failing file lives under the tsconfig "include" patterns by running tsc --showConfig and reading the effective compilerOptions.
- 03Open node_modules/<package>/package.json and check the types, typings, main, exports, and type fields for the module being imported.
- 04Compare the active moduleResolution against the project's runtime loader (Node ESM, Node CJS, Vite, Webpack) to find a resolver mismatch before editing anything.
- 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
- 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.
- 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.
- 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.
- 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.
- 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.
- 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
- 01npx tsc --noEmit exits 0 with the previously failing import still present in the source.
- 02Editor squiggles disappear after the TypeScript server reloads (or after restarting the editor process).
- 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.
- 04tsc --traceResolution shows the package being resolved to a real .d.ts or .ts file rather than the previous "Not found" line.
- 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