Declaration Files & @types Packages

How does TypeScript know the types of a library written in plain JavaScript? Through declaration files — .d.ts files that contain only type information. This lesson covers where they come from, how DefinitelyTyped's @types packages fit in, and how to write a small one yourself.

What a .d.ts file contains

A declaration file describes the shape of JavaScript code without any implementation. Here's a small JavaScript module and its matching declarations:

JS slugify.js
export function slugify(text, separator = "-") {
  return text.toLowerCase().trim().replace(/[^a-z0-9]+/g, separator);
}
export const VERSION = "1.2.0";
D.TS slugify.d.ts
export declare function slugify(text: string, separator?: string): string;
export declare const VERSION: string;

Function bodies are replaced by a ;, and values only have types. When TypeScript sees an import of ./slugify, it uses the .d.ts for checking while the bundler or runtime uses the .js for running.

Three places types come from

  1. Bundled with the package. Libraries written in TypeScript (or that ship their own declarations) include .d.ts files, pointed to by a "types" field in their package.json. Nothing extra to install — examples include Vue, Axios and date-fns.
  2. From DefinitelyTyped. For packages that don't ship types, the community maintains them in the DefinitelyTyped repository, published to npm under the @types/ scope.
  3. Written by you. For internal scripts or obscure packages, you write a small declaration file in your project.

Installing @types packages

$ terminal
npm install express
npm install --save-dev @types/express

npm install --save-dev @types/node

The @types package name mirrors the library name; for a scoped package such as @babel/core, the types live at @types/babel__core. Install them as dev dependencies, since they're only needed at build time. @types/node describes Node's built-in modules (fs, path, process) and is needed in almost every Node project.

Packages that you import find their @types automatically. Global type packages are different: since TypeScript 6.0 the types compiler option defaults to an empty list, so packages that only add globals — @types/node's process, or the describe/it globals from @types/jest — must be listed explicitly:

JSON tsconfig.json (excerpt)
{
  "compilerOptions": {
    "types": ["node", "jest"]
  }
}

If you see "Cannot find name 'process'" in a project that has @types/node installed, a missing types entry is the usual cause.

How do you know whether you need one? Install the library and import it. If its types are bundled, everything just works. If not, strict mode reports an error like this (editors usually add a hint suggesting the matching @types package):

tsc output
error TS7016: Could not find a declaration file for module 'left-pad'.
'/my-app/node_modules/left-pad/index.js' implicitly has an 'any' type.

Writing a declaration for an untyped module

If no @types package exists, create a .d.ts file anywhere in your project (a types/ folder is conventional) and describe the module:

D.TS types/left-pad.d.ts
declare module "left-pad" {
  function leftPad(str: string | number, len: number, ch?: string): string;
  export default leftPad;
}

As a quick stop-gap, declare module "left-pad"; on its own makes all imports from that package any — it silences the error but gives you no checking, so replace it with real types when you can.

Declaring globals

Scripts loaded with a <script> tag often add globals that TypeScript can't see. Describe them with declare:

D.TS globals.d.ts
declare const APP_VERSION: string;

interface Window {
  dataLayer: unknown[];
}

declare const says the variable exists at runtime without creating it. Redeclaring interface Window adds a property to the built-in Window type rather than replacing it — that's declaration merging, the topic of the next lesson.

Watch out: a .d.ts file that contains a top-level import or export becomes a module, and its declarations stop being global. To add globals from inside a module, wrap them in declare global { … }.

Generating declarations for your own code

If you publish a library written in TypeScript, let the compiler produce the .d.ts files for you:

JSON tsconfig.json (excerpt)
{
  "compilerOptions": {
    "declaration": true,
    "declarationMap": true,
    "outDir": "dist"
  }
}

Every .ts file then produces a .js and a .d.ts in dist/. declarationMap lets "go to definition" in a consumer's editor jump to your original .ts source instead of the declaration.

The built-in lib files

Even JavaScript's own built-ins — Array, Promise, document — are described by declaration files that ship with TypeScript, named like lib.es2023.d.ts and lib.dom.d.ts. The lib and target options in tsconfig.json decide which of these are loaded, which is why document is unavailable in a Node-only configuration.

skipLibCheck

Most configurations set "skipLibCheck": true. It tells TypeScript not to type-check the contents of .d.ts files — it still uses them, just doesn't validate them. This speeds up builds and avoids errors caused by conflicts between third-party declaration files you can't edit.