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:

Exact Symptoms TypeScript reports one of two diagnostics, depending on whether the module is completely unknown or just untyped: Exact Symptoms TypeScript reports one of two diagnostics, depending on whether the module is completely unknown or just untyped:
Exact Symptoms — the core idea of this section at a glance.
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.

Resolution Steps Work through these in order. Resolution Steps Work through these in order.
Resolution Steps — the core idea of this section at a glance.
  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:

Validation Verify the fix from the command line, independent of the editor's cache: Validation Verify the fix from the command line, independent of the editor's cache:
Validation — the core idea of this section at a glance.
# 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.
Prevention & CI Guardrails Prevention & CI Guardrails in production JavaScript package workflows. Prevention & CI Guardrails Prevention & CI Guardrails in production JavaScript package workflows.
Prevention & CI Guardrails — the core idea of this section at a glance.

Why the runtime resolves but the types don't

The 'cannot find module or its corresponding type declarations' error is confusing because the JavaScript often works — the module loads at runtime, but TypeScript cannot find its types. This happens because TypeScript follows the same conditional exports resolution as the runtime, and picks the declaration file under the matching condition. If a package's exports map points import and require at JavaScript but does not nest a types condition, or points at a declaration generated for the wrong format, the checker resolves the code correctly and the types not at all.

Separate paths Code and types both follow exports, separately. import works JS condition resolves types don't no types condition nest types per-condition declaration
Runtime resolution and type resolution are separate — types need their own conditions.

Under node16/nodenext resolution specifically, a .d.ts generated against the ESM build but served to a require consumer produces this error even when the runtime code resolves, because the checker expects a .d.cts under the require branch and does not find one. The mismatch is between how the package declares its types and how the consumer resolves them. Recognizing that runtime resolution and type resolution are separate paths — both following exports, but the types needing their own per-condition declarations — is what turns a baffling 'the import works but has no types' into a specific manifest fix: nest a types condition in each export branch pointing at a format-appropriate declaration.

Fixing it as a consumer and as an author

The fix depends on whether you own the package. As a consumer of a package with missing or broken types, the immediate workaround is a declaration stub — a .d.ts file in your project declaring the module — or, if the package ships types under a non-standard path, a paths mapping in your tsconfig pointing at them. For a @types/ package on DefinitelyTyped, installing it provides the declarations the package itself lacks. These get your build working without waiting on the package author.

Consumer vs author How to fix missing types on each side. Side Fix Effect consumer stub / @types / paths unblock build author wire exports types fix for all
Consumer workarounds unblock today; the author fix closes it for everyone.

As the author of the package, the durable fix is to wire the declarations correctly in your exports map so every consumer resolves them:

{
  "exports": {
    ".": {
      "import": { "types": "./dist/index.d.ts", "default": "./dist/index.mjs" },
      "require": { "types": "./dist/index.d.cts", "default": "./dist/index.cjs" }
    }
  }
}

Nesting a types condition in each branch, pointing at the format-appropriate declaration, means both import and require consumers find correct types. Verifying with @arethetypeswrong/cli against the packed tarball confirms every resolution mode resolves a declaration, so the error cannot reach a consumer. The consumer-side workarounds unblock you today; the author-side fix removes the error for everyone, which is why reporting it upstream (or fixing it if it is your package) is the resolution that actually closes the problem.

Verifying the fix resolves for every consumer

After wiring the types conditions, confirm the fix works the way every consumer will resolve it rather than trusting a local check. @arethetypeswrong/cli packs the tarball and reports whether each module-and-resolution combination — node16 import, node16 require, bundler, legacy — finds a correct declaration, so a lingering gap under one condition shows up as a specific failing cell rather than passing silently because your own tooling happens to resolve it.

Verify per mode attw and publint on the packed tarball. attw --pack every mode publint exports structure red build on gap not a consumer's editor
Validate type resolution against the artifact a consumer resolves, not the source.
- run: pnpm build
- run: pnpm exec attw --pack .
- run: pnpm exec publint

publint complements attw by checking the exports map's structure, catching a mis-ordered or malformed condition that would misroute the types. Running both in CI means the 'cannot find declarations' error becomes a red build on the pull request that introduced it, not a consumer's editor error weeks later. The principle is that type resolution, like runtime resolution, must be validated against the packed artifact a consumer resolves, because a source-importing test never exercises the published conditions where this error lives. A package whose types are verified this way in CI simply cannot ship the missing-declaration failure that is otherwise one of the most common and confusing packaging bugs.

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.

Why does the import work but TypeScript says it can't find the types?

Runtime resolution and type resolution are separate paths that both follow exports. The code resolves because the JavaScript conditions point at real files, but the types don't because the map lacks a types condition or points at a declaration for the wrong format — commonly a missing .d.cts for the require branch under node16.

How do I work around a package with missing types as a consumer?

Add a declaration stub (declare module 'pkg';) in your project, install a @types/pkg package if one exists on DefinitelyTyped, or add a paths mapping in tsconfig if the package ships types under a non-standard path. These unblock your build while you report the issue upstream.

How do I fix missing types in a package I publish?

Nest a types condition in each exports branch pointing at a format-appropriate declaration (.d.ts for import, .d.cts for require), and verify with @arethetypeswrong/cli against the packed tarball that every resolution mode resolves a declaration.

How do I confirm my types resolve for consumers under every mode?

Run @arethetypeswrong/cli --pack and publint in CI against the packed tarball. attw reports whether each resolution mode finds a correct declaration, and publint checks the exports structure, so a gap becomes a red build rather than a consumer's editor error.

Related

TypeScript Declaration Publishing