skip to content

In a TypeScript project, importing an npm package that ships only JavaScript fails with "Could not find a declaration file for module ...". What is an `@types/*` package, how does the compiler find one, and what problems does that split introduce?

level: middleimportance: must knowfreq 66%

answer

  1. some libraries ship no types at all
  2. a types-only package, no runtime code
  3. published from a community repository
  4. resolved out of node_modules/@types
  5. versioned apart from the library it describes

basics

~20 s

An @types/* package contains only declaration files for a library that ships none, published from the community DefinitelyTyped repository. TypeScript looks for it in node_modules/@types. Because it is versioned and maintained separately from the library, it can drift out of sync.

solid answer

~50 s

Some libraries bundle their own `.d.ts` files; many older JavaScript-only ones do not. For those, the community publishes declarations separately under the `@types` npm scope — `@types/lodash`, `@types/node` — sourced from the **DefinitelyTyped** repository. When you `import` a module, the compiler resolves the package normally, finds no declarations, then looks for `node_modules/@types/<name>` (walking up parent directories). If neither exists, you get the error, and the suggested fix in the message is to install `@types/<name>`. The structural problem is that the declarations are a *separate package with its own version*: they are hand-written by third parties, can lag behind the library, can describe options that no longer exist, and are not verified against the implementation. Nothing enforces agreement. When a library ships its own types, installing an `@types` package for it is redundant and can actively conflict.

go deeper

for a junior

Recognise the missing-declaration error, know that @types packages hold types only and normally belong in devDependencies, and check whether the library already ships its own before installing one.

for a middle

Explain the resolution order — bundled declarations first, then node_modules/@types walking up — and the scoped-name flattening, plus why the automatic global inclusion of type roots makes @types/node different.

for a senior

Talk about drift as an operational risk: declarations are unverified hand-written claims on their own release train, so a passing build does not prove a call is safe, and duplicate @types versions are a real source of confusing errors.

for a principal

Own the dependency policy — whether @types belong in dependencies for a library you publish, how you keep them aligned across a monorepo, and when to contribute upstream instead of maintaining local overrides.

## Where declarations come from There are exactly three ways your compiler can know the shape of a dependency: 1. **The package ships them.** Its own `.d.ts` files, emitted from its TypeScript source or hand-written, advertised through `package.json`. This is the modern default. 2. **A separate `@types` package.** Declarations only, no runtime code, published under the `@types` npm scope. 3. **You write them.** A local ambient declaration in your own repo. Option 2 exists because TypeScript arrived long after most of npm. **DefinitelyTyped** is a large community repository holding declarations for thousands of JavaScript libraries; each folder in it is published automatically to npm as `@types/<name>`. Installing `@types/lodash` adds no code to your bundle at all — the package is nothing but types, and it belongs in `devDependencies` for an application. ## How the compiler finds them For `import { x } from 'somepkg'`, resolution proceeds roughly as: locate `node_modules/somepkg`, consult its `package.json` for declarations, and if there are none, look for `node_modules/@types/somepkg`, walking up the directory tree the same way Node walks up for packages. Scoped packages are flattened with a double underscore: declarations for `@scope/pkg` live in `@types/scope__pkg`. There is a second, separate mechanism. Packages under the type roots (by default every `@types` folder found walking up from the project) are also included **globally**, not just when imported — which is how `@types/node` makes `process` and `Buffer` available without an import. The `types` compiler option narrows that automatic global inclusion to a named list; it does **not** stop `import`-driven resolution from finding `@types` packages. Without any of this, and with `noImplicitAny` on, the compiler reports that it could not find a declaration file for the module and suggests installing `@types/<name>`. ## The problems the split creates **Version drift.** `@types/[email protected]` and `[email protected]` are unrelated release trains. The declarations may miss a new option, describe a removed one, or model an old signature. Since nothing checks declarations against implementation, the compiler will confidently green-light a call that throws at runtime, and equally reject a call that works fine. **Hand-written, not derived.** These declarations are written by volunteers reading documentation. They are frequently excellent and occasionally wrong — over-permissive (`any` where a union belongs) or over-strict. **Duplicates and conflicts.** Two dependencies pulling different major versions of the same `@types` package can end up with two copies declaring the same globals, producing duplicate-identifier errors inside declaration files — which is one of the classic reasons teams reach for `skipLibCheck`. **Redundancy.** If a library now bundles its own types, a leftover `@types` entry in your `package.json` is dead weight and can shadow or conflict with the real ones. DefinitelyTyped marks such packages as stubs, and removing them is the fix. ## Practical rules - Check whether the library bundles types **before** installing `@types` for it; the newer the library, the more likely it does. - Keep the `@types` version aligned with the library's major version where the maintainers track it, and treat a mismatch as a real risk rather than a lint nit. - `@types/node` is special: pin it near the Node version you actually run, because it also decides which globals exist. - If no declarations exist anywhere, you are into hand-written ambient declarations — a different mechanism with its own tradeoffs. - Treat an `@types` declaration as an unverified claim. When it disagrees with the library's real behaviour, the library is right.

  • Should `@types/*` packages be a dependency or a devDependency?
    For an application, `devDependencies` — they contain no runtime code and consumers never install your app. For a **library** whose published `.d.ts` files reference types from an `@types` package in their public signatures, they must be a real `dependency` (or a peer), otherwise your consumers' builds cannot resolve the types your declarations mention.
  • How does the compiler resolve declarations for a scoped package like `@scope/pkg`?
    Scoped names are flattened with a double underscore, so the declarations live at `node_modules/@types/scope__pkg`. Resolution otherwise works the same: the package's own bundled declarations win, and the `@types` lookup is the fallback when it ships none.
  • Why is `@types/node` in a different category from most `@types` packages?
    Because it declares **globals**, not just an importable module — `process`, `Buffer`, `__dirname`, and the built-in module shapes. It is included automatically from the type roots rather than only on import, so its version effectively decides which runtime globals the checker believes exist, and it should be pinned near the Node version you actually run.
  • What does it mean when a library's declarations and its actual behaviour disagree?
    The declarations are wrong — they are a separate, hand-written artifact that nothing verifies against the implementation. Types are erased at compile time and constrain nobody at runtime, so a green build proves only that your calls match somebody's description of the library. Trust the library, and fix or override the declaration.

saying these in an interview costs you the question

  • Thinks @types packages contain runtime code that ships to production
  • Assumes the @types version always matches the library version
  • Believes DefinitelyTyped declarations are generated from the source
  • Installs @types for a library that already bundles its own types
  • Says a green type check proves the call is correct at runtime

context