Back to core workflows Fix dependency resolution Tune package metadata Jump to monorepo patterns

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 types entirely. The package ships no .d.ts, no top-level types field, and no types condition in exports. There are no community @types/x either. Result: ts(7016).
  • Wrong exports types condition order. The package ships declarations, but the types key sits after default/import/require inside exports. Under node16/bundler the resolver matches the JS first and never reaches types, producing ts(2307) or ts(7016).
  • moduleResolution mismatch. Your tsconfig uses moduleResolution: "node" (legacy), which ignores exports. A modern package that exposes types only through exports conditions becomes invisible, yielding ts(2307).
  • Missing @types package. An untyped library has its declarations in a separate @types/x package that is not installed.
  • A .d.mts/.d.cts mismatch. The package ships a single .d.ts but is consumed under node16, where the ESM or CJS condition expected a format-specific declaration. This overlaps with Generating Dual CJS/ESM Type Definitions.
Declaration resolution decision tree A decision flow from the error to the missing-types root cause and its fix. ts(2307) / ts(7016) types not found Does pkg ship .d.ts? check node_modules No types shipped install @types/x Types present fix exports order Legacy resolution set node16/bundler
Triage path: first confirm whether the package ships declarations, then branch to the matching fix.

Resolution Steps

Work through these in order. The first that applies is usually the fix.

  1. Confirm what the package actually ships. Inspect the installed package's manifest and look for a types field and types conditions in exports:

    cat node_modules/x/package.json | grep -E '"(types|typings|exports)"' -A3
    ls node_modules/x/dist/*.d.* 2>/dev/null

    If there are no .d.ts/.d.mts/.d.cts files anywhere, the package is untyped — go to step 2. If declarations exist, go to step 3.

  2. Install community types, or declare the module yourself. Many untyped libraries have a @types/x package:

    npm install --save-dev @types/x

    If none exists, add a local ambient declaration so the import is at least typed as any intentionally:

    // types/x.d.ts  (ensure this dir is in tsconfig "include" or typeRoots)
    declare module 'x';
  3. Fix exports ordering (if you own the package). The types condition 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 types first inside each format, as detailed in Generating Dual CJS/ESM Type Definitions.

  4. Align moduleResolution in your tsconfig.json. If the package only exposes types through exports, a legacy node resolver cannot see them. Switch to a resolver that reads exports:

    {
      "compilerOptions": {
        "module": "NodeNext",
        "moduleResolution": "NodeNext"
      }
    }

    For bundler-driven apps use "moduleResolution": "bundler" with "module": "ESNext". Both read exports types conditions; legacy "node" does not.

  5. 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 --noEmit in CI on every pull request so a resolution regression fails the build, not a consumer.
  • Add an @arethetypeswrong/cli --pack step before publish to catch missing or misordered types conditions automatically.
  • Keep a one-file consumer smoke test that installs the packed tarball under moduleResolution: "NodeNext" and type-checks an import.
  • Pin typescript and your build tool versions so resolution behavior does not drift between contributors.
  • When you own the package, always place types first in each exports condition 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