Fixing 'Cannot Find Module' Type Declaration Errors
A package installs cleanly, the import runs at runtime, and yet TypeScript paints the import statement red. This is a type-resolution failure, not a runtime failure — the compiler cannot find a .d.ts for the module. This page covers the two exact errors you will see, why each happens, and the minimal patch that resolves them.
Exact Symptoms
TypeScript reports one of two diagnostics, depending on whether the module is completely unknown or just untyped:
Cannot find module 'x' or its corresponding type declarations. ts(2307)
Could not find a declaration file for module 'x'. '/path/to/node_modules/x/dist/index.js'
implicitly has an 'any' type.
Try `npm i --save-dev @types/x` if it exists or add a new declaration (.d.ts)
file containing `declare module 'x';` ts(7016)
The distinction matters. ts(2307) means resolution found nothing usable at all — no JS entry and no types under the active resolution mode. ts(7016) means resolution found the JavaScript but no matching declarations, so the module is implicitly any.
Root Cause Analysis
Type resolution runs the same moduleResolution algorithm as runtime resolution, but it looks for declarations instead of executable files. It breaks for one of these reasons, which trace back to how a package wires up TypeScript Declaration Publishing:
- Missing
typesentirely. The package ships no.d.ts, no top-leveltypesfield, and notypescondition inexports. There are no community@types/xeither. Result:ts(7016). - Wrong
exportstypescondition order. The package ships declarations, but thetypeskey sits afterdefault/import/requireinsideexports. Undernode16/bundlerthe resolver matches the JS first and never reachestypes, producingts(2307)orts(7016). moduleResolutionmismatch. YourtsconfigusesmoduleResolution: "node"(legacy), which ignoresexports. A modern package that exposes types only throughexportsconditions becomes invisible, yieldingts(2307).- Missing
@typespackage. An untyped library has its declarations in a separate@types/xpackage that is not installed. - A
.d.mts/.d.ctsmismatch. The package ships a single.d.tsbut is consumed undernode16, where the ESM or CJS condition expected a format-specific declaration. This overlaps with Generating Dual CJS/ESM Type Definitions.
Resolution Steps
Work through these in order. The first that applies is usually the fix.
-
Confirm what the package actually ships. Inspect the installed package's manifest and look for a
typesfield andtypesconditions inexports:cat node_modules/x/package.json | grep -E '"(types|typings|exports)"' -A3 ls node_modules/x/dist/*.d.* 2>/dev/nullIf there are no
.d.ts/.d.mts/.d.ctsfiles anywhere, the package is untyped — go to step 2. If declarations exist, go to step 3. -
Install community types, or declare the module yourself. Many untyped libraries have a
@types/xpackage:npm install --save-dev @types/xIf none exists, add a local ambient declaration so the import is at least typed as
anyintentionally:// types/x.d.ts (ensure this dir is in tsconfig "include" or typeRoots) declare module 'x'; -
Fix
exportsordering (if you own the package). Thetypescondition must precede the JS conditions. This is the highest-frequency cause when declarations exist but are not found:{ "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.mjs", "require": "./dist/index.cjs" } } }For dual packages, nest
typesfirst inside each format, as detailed in Generating Dual CJS/ESM Type Definitions. -
Align
moduleResolutionin yourtsconfig.json. If the package only exposes types throughexports, a legacynoderesolver cannot see them. Switch to a resolver that readsexports:{ "compilerOptions": { "module": "NodeNext", "moduleResolution": "NodeNext" } }For bundler-driven apps use
"moduleResolution": "bundler"with"module": "ESNext". Both readexportstypesconditions; legacy"node"does not. -
Restart the TS language server. Editors cache resolution. After installing types or editing
tsconfig, restart the TypeScript server so the editor re-resolves.
Worked example: a package that runs but is untyped
Suppose import { parse } from 'fast-thing' runs at runtime but TypeScript reports ts(7016). Inspecting node_modules/fast-thing/package.json shows an exports map with import and require conditions but no types key anywhere. The package author shipped JS only. Because the JS resolves, you get ts(7016) (untyped) rather than ts(2307) (nothing found). There is no @types/fast-thing on the registry, so the immediate unblock is a local ambient declaration:
// types/fast-thing.d.ts
declare module 'fast-thing' {
export function parse(input: string): unknown;
}
This is your declaration, not the author's, so keep it minimal and accurate to the surface you actually call. File an upstream request for real types so you can delete the shim later. The contrast is instructive: when the JS also fails to resolve — for example a node resolver against an exports-only package — the same missing import surfaces as ts(2307) instead, and the fix is step 4, not a shim.
Validation
Verify the fix from the command line, independent of the editor's cache:
# Compiler-truth: does the project type-check with the current config?
npx tsc --noEmit -p tsconfig.json
To confirm a package you publish resolves under every consumer mode, simulate resolution against the packed tarball with an "are the types wrong" style check:
# Pack exactly what npm would publish, then probe every resolution mode
npm pack
npx --yes @arethetypeswrong/cli ./*.tgz --format table
A green result for node16 (ESM and CJS) and bundler rows means every consumer can find your types. Red rows name the exact failing mode.
Prevention & CI Guardrails
- Run
tsc --noEmitin CI on every pull request so a resolution regression fails the build, not a consumer. - Add an
@arethetypeswrong/cli --packstep before publish to catch missing or misorderedtypesconditions automatically. - Keep a one-file consumer smoke test that installs the packed tarball under
moduleResolution: "NodeNext"and type-checks an import. - Pin
typescriptand your build tool versions so resolution behavior does not drift between contributors. - When you own the package, always place
typesfirst in eachexportscondition object — make it a review checklist item.
Frequently Asked Questions
What is the difference between ts(2307) and ts(7016)?
ts(2307) means resolution found no usable module at all under the active mode — neither JS nor types resolved. ts(7016) means the JavaScript resolved but no declarations were found, so the import is implicitly any. The first usually points at an exports/moduleResolution mismatch; the second at genuinely missing types.
Why does the import run fine at runtime but TypeScript still cannot find it?
Runtime resolution and type resolution are separate passes over the same exports map. The runtime conditions (import/require/default) can resolve perfectly while the types condition is missing, misordered, or points at a nonexistent file. Fix the types condition specifically.
Is declare module 'x'; a real fix?
It is a last resort. It silences the error by typing the module as any, sacrificing all type safety. Use it only when no real declarations or @types/x exist, and prefer writing accurate ambient types if the surface is small.
I switched to moduleResolution: "node16" and now I see more errors. Why?
node16 is stricter: it reads exports, requires explicit file extensions in relative imports, and distinguishes .d.mts from .d.cts. The new errors are real problems that legacy node resolution silently ignored. Fix them rather than reverting.
Related
- TypeScript Declaration Publishing — how the
typesfield andexportsconditions are meant to be wired so resolution succeeds. - Generating Dual CJS/ESM Type Definitions — the format-specific
.d.mts/.d.ctsthat prevent masquerading errors undernode16. - Understanding package.json Fields — the manifest fields involved in declaration resolution.
- ESM and CJS Interoperability — why ESM and CJS consumers resolve modules differently.