TypeScript Declaration Files

A declaration file (.d.ts) describes the shape of JavaScript code (its functions, classes, and exports) without any implementation. It's how TypeScript type-checks calls into plain JavaScript libraries, browser APIs, and Node built-ins. Every time you hover over fetch, Array.prototype.map, or a function from an npm package, you're reading a declaration file.

Most of the time they're invisible: libraries ship their own types or you install @types/*. You need to understand them when a package has no types, when you want to extend a library's types (adding a field to Express's Request, for example), and when you publish your own package.

TL;DR

Quick Example

Typing an untyped package, and augmenting a library's types:

Core Concepts

What Goes in a .d.ts

Declaration files use declare to say "this exists at runtime; trust me about its type":

They can't contain executable code. TypeScript never emits JavaScript from them.

Where Types Come From

  1. Bundled lib files: the lib option in tsconfig pulls in lib.es2023.d.ts, lib.dom.d.ts, and others.
  2. The package itself: modern packages ship .d.ts files and point to them from package.json ("types" or a "types" condition in "exports"). npm shows a "TS" badge for these.
  3. DefinitelyTyped: a community repository publishing @types/<package> for libraries that don't ship types. TypeScript picks up @types/* packages in node_modules automatically.

If none exist, TypeScript reports "Could not find a declaration file for module 'foo'" and types the import as any or errors under noImplicitAny.

Ambient Module Declarations

declare module "name" { ... } describes a module by its import specifier. It's also used for non-code imports that bundlers handle:

A shorthand declare module "foo"; with no body types everything from foo as any. It's a quick unblock, but it removes all safety.

Declaration Merging and Module Augmentation

TypeScript merges multiple declarations of the same interface (and namespace) into one. Combined with declare module, that lets you extend types from another package:

For augmentation to apply, the file must be a module (containing at least one import or export) and be included in the compilation.

Global Declarations

To add something truly global, like a variable injected by a script tag or a property on window:

Publishing Types With a Package

For TypeScript source, let the compiler generate declarations:

Put "types" first in each export condition. Validate with tools like @arethetypeswrong/cli, which catch mismatches between your types and what Node or bundlers will actually load. Bundlers like tsup, tsdown, and unbuild can also emit rolled-up declarations. declarationMap enables "Go to Definition" to jump to your source instead of the .d.ts.

Best Practices

Prefer Packages That Ship Their Own Types

First-party types track the library's versions exactly. @types/* packages are maintained separately and can lag or mismatch. When choosing between libraries, bundled types are a quality signal.

Keep Local Declarations in One Folder

Put hand-written .d.ts files in src/types/ and make sure include covers it. Name them after what they describe (express.d.ts, env.d.ts) so augmentations are easy to find.

Type Only What You Use

When writing declarations for an untyped library, describe the functions you actually call, precisely. A small accurate declaration beats a large guessed one. Consider contributing it to DefinitelyTyped.

Keep @types Versions Aligned

@types/node should match your Node major version, and @types/react your React version. Mismatches cause confusing errors about APIs that "don't exist" or have the wrong signature.

Common Mistakes

Augmentation File That Isn't a Module

Silencing Everything With declare module "*"

A wildcard declare module "*"; makes every import any, including typos in package names. Declare specific modules instead.

Editing Files in node_modules/@types

Changes vanish on the next install. Use module augmentation in your own .d.ts, or patch-package for genuine fixes, and upstream the fix to DefinitelyTyped.

FAQ

What's the difference between .ts and .d.ts?

A .ts file contains code plus types and compiles to JavaScript. A .d.ts file contains only type declarations, produces no output, and describes JavaScript that exists elsewhere, whether in a compiled library, a browser API, or a script loaded at runtime.

How do I fix "Could not find a declaration file for module"?

Try npm i -D @types/<package> first. If that doesn't exist, check whether a newer version of the package ships its own types. Otherwise add a local declare module "<package>" with the signatures you use, or the bodyless shorthand to unblock yourself temporarily.

Do I need @types for packages written in TypeScript?

No. Packages written in TypeScript, or that ship their own .d.ts files, provide types automatically. Installing a stale @types package for them can even conflict. Check the package's package.json for a "types" field or exports condition.

How do I type environment variables?

For Node, augment NodeJS.ProcessEnv in a global declaration. For Vite, extend ImportMetaEnv in vite-env.d.ts. Better still, validate env vars at startup with a schema (for example Zod) and export a typed config object.

Related Topics

References