Question

Fix 'Could not find a declaration file for module' in TypeScript

typescriptnode-modules

Question

I have a TypeScript package that compiles into a dist folder containing both JavaScript files and declaration files:

├── dist
│   ├── annotations.d.ts
│   ├── annotations.js
│   ├── index.d.ts
│   ├── index.js
│   ├── injector.d.ts
│   ├── injector.js
│   ├── profiler.d.ts
│   ├── profiler.js
│   ├── providers.d.ts
│   ├── providers.js
│   ├── util.d.ts
│   └── util.js
├── LICENSE
├── package.json
├── README.md
├── src
│   ├── annotations.ts
│   ├── index.ts
│   ├── injector.ts
│   ├── profiler.ts
│   ├── providers.ts
│   └── util.ts
└── tsconfig.json

In package.json, I set:

{
  "main": "dist/index.js"
}

When I use the package in Node.js, it works correctly. However, in TypeScript, this import causes an error:

import { Injector } from '@ts-stack/di';

Error:

Could not find a declaration file for module '@ts-stack/di'. '/path/to/node_modules/@ts-stack/di/dist/index.js' implicitly has an 'any' type.

But if I import the file directly, TypeScript works:

import { Injector } from '/path/to/node_modules/@ts-stack/di/dist/index.js';

Why does TypeScript fail to find the declaration file when importing the package name, and how should this package be configured correctly?

Short Answer

By the end of this page, you will understand why TypeScript can run a JavaScript package but still fail to find its type declarations, how package.json fields such as main and types affect module resolution, and how to properly publish a TypeScript package so consumers can import it by package name without errors.

Concept

TypeScript resolves runtime code and type information separately.

Node.js only cares about the JavaScript entry point, usually defined by the main field in package.json:

{
  "main": "dist/index.js"
}

That is enough for Node to execute your package.

TypeScript, however, also needs to know where the declaration files are. Declaration files are the .d.ts files that describe the public API of your package.

If TypeScript can load the JavaScript file but cannot find matching type declarations, it treats the module as any and shows an error like:

Could not find a declaration file for module 'package-name'

Even if dist/index.d.ts exists, TypeScript may not automatically use it when importing the package by name unless the package metadata clearly points to it.

The usual fix is to add a types field to package.json:

{
  "main": "dist/index.js",
  "types": "dist/index.d.ts"
}

This tells TypeScript exactly where the entry declaration file is.

Why this matters:

  • Consumers get autocomplete and type checking.
  • Your package becomes easy to use from TypeScript projects.
  • Imports work the same way for both JavaScript and TypeScript users.
  • You avoid forcing users to import internal file paths like dist/index.js.

In short:

  • main is for JavaScript runtime resolution.
  • types (or typings) is for TypeScript declaration resolution.
  • A published TypeScript package should usually provide both.

Mental Model

Think of a package like a book:

  • main tells Node.js which chapter to start reading.
  • types tells TypeScript where the table of contents and glossary are.

Without types, Node can still read the book, but TypeScript cannot understand the meaning of the terms inside it.

So your package may run fine but still be invisible to the type system.

Take Quiz

Syntax and Examples

A TypeScript-friendly package usually includes both JavaScript output and declaration files.

Correct package.json

{
  "name": "@ts-stack/di",
  "main": "dist/index.js",
  "types": "dist/index.d.ts"
}

Basic library structure

dist/
  index.js
  index.d.ts

Example library code

Source:

// src/index.ts
export class Injector {
  get(name: string): string {
    return `Resolved: ${name}`;
  }
}

Compiled output:

// dist/index.js
export class Injector {
  get(name) {
    return `Resolved: ${name}`;
  }
}

Generated declaration:

// dist/index.d.ts
export declare class Injector {
  get(name: string): string;
}

Consumer import

import { Injector } from '@ts-stack/di';

const injector = new Injector();
console.log(injector.get('UserService'));

If the package has a valid types entry, TypeScript understands Injector correctly.

Older alias

Some packages use typings instead of types:

{
  "main": "dist/index.js",
  "typings": "dist/index.d.ts"
}

types is the more common modern choice.

Step by Step Execution

Consider this package metadata:

{
  "main": "dist/index.js"
}

And this import:

import { Injector } from '@ts-stack/di';

What happens step by step

  1. TypeScript sees the package import @ts-stack/di.
  2. It looks inside that package's package.json.
  3. It finds main: "dist/index.js", so it knows which JavaScript file is the runtime entry.
  4. It then tries to find type declarations for the package.
  5. If there is no types field and TypeScript cannot confidently resolve the declarations for the package entry, it reports that the module has no declaration file.
  6. Because types are missing, the package is treated as any.

Why direct file import may appear to work

import { Injector } from '/path/to/node_modules/@ts-stack/di/dist/index.js';

In this case, TypeScript is resolving a specific file path. Since index.d.ts sits next to index.js, TypeScript can often match them directly.

So:

  • Package import uses package metadata.
  • File import uses file-based resolution.

That is why one can fail while the other succeeds.

Fixed flow

With this configuration:

{
  "main": "dist/index.js",
  "types": "dist/index.d.ts"
}

The steps become:

  1. TypeScript reads the package import.
  2. It finds main for runtime code.
  3. It finds types for declaration entry.
  4. It loads dist/index.d.ts.
  5. The import is fully typed.

Real World Use Cases

This issue appears often when building and publishing TypeScript libraries.

Shared internal packages

A team publishes a utility package used by several services:

import { formatCurrency } from '@company/utils';

If types is missing, every TypeScript project importing it loses autocomplete and type safety.

Open-source npm libraries

A library author publishes compiled JavaScript and .d.ts files, but forgets to include the declaration entry in package.json. JavaScript users are fine, but TypeScript users get errors.

Monorepos

In a workspace setup, one package may depend on another local package. If the dependent package is not configured with correct declaration metadata, imports work at runtime but fail in editors and builds.

SDKs and API clients

Packages that expose classes, functions, and config objects rely heavily on declaration files so users can discover the API without reading source code.

Take Quiz

Real Codebase Usage

In real projects, developers usually combine several patterns to make package typing reliable.

Explicit package entry points

The most common setup is:

{
  "main": "dist/index.js",
  "types": "dist/index.d.ts"
}

This makes the package entry predictable.

Build declarations automatically

In tsconfig.json:

{
  "compilerOptions": {
    "declaration": true,
    "outDir": "dist"
  }
}

This ensures .d.ts files are generated whenever the package builds.

Export from a single public entry file

Developers usually re-export public APIs from src/index.ts:

export { Injector } from './injector';
export { Provider } from './providers';

Then the generated dist/index.d.ts becomes the public type entry.

Guard against publishing mistakes

Teams often verify published package contents with:

npm pack

Then inspect the tarball to confirm that:

  • dist/index.js exists
  • dist/index.d.ts exists
  • package.json contains the right fields

Use exports in modern packages

Some codebases also define explicit export maps:

{
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "default": "./dist/index.js"
    }
  }
}

This is common in newer packages, though the core lesson remains the same: runtime and type entry points should both be defined clearly.

Common Mistakes

1. Setting only main

Broken:

{
  "main": "dist/index.js"
}

Why it fails:

  • Node can run the package.
  • TypeScript may not know where the declarations start.

Fix:

{
  "main": "dist/index.js",
  "types": "dist/index.d.ts"
}

2. Forgetting to generate declaration files

Broken tsconfig.json:

{
  "compilerOptions": {
    "outDir": "dist"
  }
}

Fix:

{
  "compilerOptions": {
    "declaration": true,
    "outDir": "dist"
  }
}

3. Publishing without the .d.ts files

Your local build may work, but the published npm package may not include declaration files.

Avoid this by checking:

  • .npmignore
  • files in package.json
  • output of npm pack

4. Importing internal paths instead of the package entry

Avoid this:

import { Injector } from '@ts-stack/di/dist/index.js';

Why it is a problem:

  • It bypasses the public API.
  • It couples users to your folder structure.
  • Refactoring becomes harder.

Prefer:

import { Injector } from '@ts-stack/di';

5. Assuming JavaScript success means TypeScript success

Runtime success only proves that the JavaScript file exists. It does not prove that declaration files are discoverable.

6. Wrong declaration path

Broken:

{
  "types": "index.d.ts"
}

If the actual file is in dist/index.d.ts, TypeScript still fails.

Always match the real published path exactly.

Comparisons

ConceptPurposeUsed ByExample
mainJavaScript runtime entryNode.js, bundlers"main": "dist/index.js"
typesTypeScript declaration entryTypeScript"types": "dist/index.d.ts"
typingsOlder alias for typesTypeScript"typings": "dist/index.d.ts"
direct file importImport a concrete file pathRuntime and TS file resolutionimport x from './dist/index.js'
package importImport via package namePackage resolutionimport x from 'my-lib'

Package import vs direct file import

StyleHow it resolvesProsCons
import { X } from 'my-lib'Uses package.json metadataClean public API, stableRequires proper package config
import { X } from 'my-lib/dist/index.js'Uses file pathCan work around bad configLeaks internal structure

types vs @types/...

SituationWhat to do
You own the package and it is written in TypeScriptPublish .d.ts files and set types
Package is JavaScript and has no built-in typesInstall @types/package-name if available
No declarations exist anywhereCreate a custom .d.ts as a temporary workaround
Take Quiz

Cheat Sheet

{
  "main": "dist/index.js",
  "types": "dist/index.d.ts"
}

Key rules

  • main points to the runtime JavaScript entry.
  • types points to the TypeScript declaration entry.
  • Generate declarations with declaration: true.
  • Publish the .d.ts files along with .js files.
  • Prefer importing the package name, not internal file paths.

Minimal tsconfig.json

{
  "compilerOptions": {
    "declaration": true,
    "outDir": "dist"
  }
}

Minimal package structure

dist/
  index.js
  index.d.ts
package.json

Typical fix for this error

  1. Build declaration files.
  2. Add "types": "dist/index.d.ts" to package.json.
  3. Republish the package.
  4. Reinstall it in the consumer project.

Common error meaning

implicitly has an 'any' type

TypeScript found JavaScript code but not usable type declarations.

FAQ

Why does Node.js work but TypeScript fails?

Node.js only needs JavaScript to run. TypeScript also needs declaration files for type checking.

Is main enough for a TypeScript package?

No. main is for runtime code. You should also provide types for declarations.

Do I need @types/... for my own TypeScript library?

Usually no. If your package generates and publishes its own .d.ts files, that is enough.

What should the types field point to?

It should point to your package's public declaration entry, usually something like dist/index.d.ts.

Can TypeScript infer types from JavaScript automatically?

Sometimes partially, but published packages should not rely on that. Declaration files are the standard solution.

What is the difference between types and typings?

They serve the same purpose. types is the common modern field name.

Why does importing dist/index.js work?

Because TypeScript can often match that exact file with a nearby index.d.ts. That does not mean the package itself is configured correctly.

Should users import from dist/... directly?

No. They should import from the package name. Internal paths should stay private unless intentionally documented.

Take Quiz

Mini Project

Description

Create a tiny TypeScript library called math-tools and configure it so another TypeScript file can import it by package name without any declaration errors. This demonstrates the exact packaging pattern needed for reusable libraries.

Goal

Build and configure a package with both runtime and type entry points so TypeScript consumers get correct imports and autocomplete.

Requirements

  • Create a library entry file that exports at least one function or class.
  • Compile the library to a dist folder and generate declaration files.
  • Configure package.json with both main and types.
  • Write a consumer file that imports the package by name.
  • Ensure the import works without using internal dist/... paths.
Take Quiz

Keep learning

Related questions

View more