Types must come from somewhere. For your own code TypeScript reads the .ts sources; for compiled libraries, global scripts and values injected at build time it needs declaration files (.d.ts) that describe the shapes without any implementation. This lesson explains how TypeScript finds types for npm packages, how to install community typings from DefinitelyTyped, how to write your own declarations, and how to generate them for a library you publish.
A declaration file contains only types and declare statements. declare says "this exists at runtime, trust me" and emits nothing:
// vendor.d.ts
declare const VERSION: string;
declare function track(event: string, data?: object): void;
declare class Widget {
constructor(el: HTMLElement);
render(): void;
}
declare namespace Legacy {
function init(config: { debug: boolean }): void;
}Interfaces and type aliases need no declare because they are already type-only. A .d.ts file with no top-level import or export is global: its declarations are visible everywhere. Add one import and it silently becomes a module, the most common cause of a declaration "stopping working".
Any .d.ts under a folder matched by include in tsconfig.json is picked up automatically; a conventional location is src/types/.
When you import x from "pkg", TypeScript looks, in order, for:
"types" (or "typings") field in the package's package.json, or a "types" condition inside its "exports" map.index.d.ts next to the package's main file.node_modules/@types/pkg package.Many libraries ship their own types (step 1 and 2). For the rest, the DefinitelyTyped repository publishes community-maintained declarations under the @types scope:
npm install --save-dev @types/node # Node.js built-ins: fs, path, process
npm install --save-dev @types/express # typings for the express package
npm install --save-dev @types/lodashInstall them as dev dependencies; they are needed for compilation, not at runtime. Keep the major version aligned with the library (express@5 with @types/express@5). If a package has neither bundled types nor an @types entry, the import is an error under strict ("Could not find a declaration file for module"), and you write a shorthand declaration yourself, as in the sample at the top.
Two tsconfig.json options affect this lookup:
| Option | Purpose |
|---|---|
| types: ["node", "vitest/globals"] | Restricts which @types packages are loaded globally; default is all of them |
| skipLibCheck: true | Skips type-checking .d.ts files, speeding builds and avoiding conflicts between third-party typings |
Declaration files can extend types you do not own. Interfaces merge, so a global interface such as Window can be widened from a global .d.ts. For module-scoped types, use module augmentation:
// src/types/express.d.ts
import "express";
declare module "express-serve-static-core" {
interface Request {
user?: { id: string; role: "admin" | "member" };
}
}The import "express" makes the file a module; declare module "express-serve-static-core" reopens the package that actually defines Request and merges the new property in. Every req.user in the project is now typed. The same technique adds custom theme keys to styling libraries and extra fields to ORM models.
To add globals from a module file, wrap them: declare global { interface Window { ... } }.
If you publish a package, emit .d.ts files next to the JavaScript so consumers get types automatically:
{
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"outDir": "dist"
}
}Then point package.json at them:
{
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" } }
}declarationMap lets "Go to Definition" jump to your .ts source instead of the .d.ts. emitDeclarationOnly produces only declarations when another tool (esbuild, swc) handles the JavaScript. TypeScript 5.5's isolatedDeclarations requires explicit types on exported APIs so declarations can be generated file-by-file by fast tools.
import to a global .d.ts and losing the globals; use declare global instead.@types/* as a regular dependency, or forgetting to update it after upgrading the library.node_modules/@types; put an augmentation in src/types/ so the fix survives reinstalls.Where does TypeScript look for the types of an npm package that does not bundle its own?
.d.ts files describe types without implementation; declare marks values that exist at runtime.package.json types/exports, then falls back to @types/* from DefinitelyTyped.@types packages as dev dependencies and keep their major versions aligned with the libraries.declare module "..." let you augment third-party and global types safely from your own files.declaration: true (plus declarationMap) generates typings for packages you publish.Next lesson: async/await and Typing Promises — type asynchronous code, unwrap results and handle concurrency safely.