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:
mainis for JavaScript runtime resolution.types(ortypings) is for TypeScript declaration resolution.- A published TypeScript package should usually provide both.
Mental Model
Think of a package like a book:
maintells Node.js which chapter to start reading.typestells 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.
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
- TypeScript sees the package import
@ts-stack/di. - It looks inside that package's
package.json. - It finds
main: "dist/index.js", so it knows which JavaScript file is the runtime entry. - It then tries to find type declarations for the package.
- If there is no
typesfield and TypeScript cannot confidently resolve the declarations for the package entry, it reports that the module has no declaration file. - 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:
- TypeScript reads the package import.
- It finds
mainfor runtime code. - It finds
typesfor declaration entry. - It loads
dist/index.d.ts. - 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.
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.jsexistsdist/index.d.tsexistspackage.jsoncontains 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:
.npmignorefilesinpackage.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
| Concept | Purpose | Used By | Example |
|---|---|---|---|
main | JavaScript runtime entry | Node.js, bundlers | "main": "dist/index.js" |
types | TypeScript declaration entry | TypeScript | "types": "dist/index.d.ts" |
typings | Older alias for types | TypeScript | "typings": "dist/index.d.ts" |
| direct file import | Import a concrete file path | Runtime and TS file resolution | import x from './dist/index.js' |
| package import | Import via package name | Package resolution | import x from 'my-lib' |
Package import vs direct file import
| Style | How it resolves | Pros | Cons |
|---|---|---|---|
import { X } from 'my-lib' | Uses package.json metadata | Clean public API, stable | Requires proper package config |
import { X } from 'my-lib/dist/index.js' | Uses file path | Can work around bad config | Leaks internal structure |
types vs @types/...
| Situation | What to do |
|---|---|
| You own the package and it is written in TypeScript | Publish .d.ts files and set types |
| Package is JavaScript and has no built-in types | Install @types/package-name if available |
| No declarations exist anywhere | Create a custom .d.ts as a temporary workaround |
Cheat Sheet
{
"main": "dist/index.js",
"types": "dist/index.d.ts"
}
Key rules
mainpoints to the runtime JavaScript entry.typespoints to the TypeScript declaration entry.- Generate declarations with
declaration: true. - Publish the
.d.tsfiles along with.jsfiles. - 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
- Build declaration files.
- Add
"types": "dist/index.d.ts"topackage.json. - Republish the package.
- 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.
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
distfolder and generate declaration files. - Configure
package.jsonwith bothmainandtypes. - Write a consumer file that imports the package by name.
- Ensure the import works without using internal
dist/...paths.
Keep learning
Related questions
@Directive vs @Component in Angular: Differences, Use Cases, and When to Use Each
Learn the difference between @Directive and @Component in Angular, including use cases, examples, and when to choose each.
Accessing Input Value from EventTarget in TypeScript
Learn why EventTarget has no value property in TypeScript and safely read values from HTML input events in Angular applications.
Angular (change) vs (ngModelChange): What’s the Difference?
Learn the difference between Angular (change) and (ngModelChange), when each fires, and which one to use in forms and inputs.