Declaration Files & DefinitelyTyped
Intermediate · 20 min read · ▶ live playground · ✦ checkpoint
Declaration files are how TypeScript understands JavaScript it cannot see. A .d.ts file contains types without implementation, so your editor can autocomplete a package, check your calls, and show hovers even when the runtime code is plain .js.
This is a tool-shaped lesson. Package lookup and installed type packages are project behavior, so the examples are static files and terminal transcripts rather than playgrounds.
.d.ts files are type promises for JavaScript
A declaration file is a .d.ts file: a type-only description of values that exist somewhere else at runtime. It does not contain function bodies. It tells the checker, "when code refers to this name, here is the shape you should expect."
For a legacy script loaded before your app, a declaration file might look like this:
type LegacyPlan = "free" | "pro";
type LegacyMember = {
id: string;
plan: LegacyPlan;
karma: number;
};
declare function readLegacyMember(id: string): LegacyMember;
declare const legacyBuild: {
readonly version: string;
};There are two important words in that example.
declare means "this value exists at runtime, but not because this file creates it." TypeScript trusts the shape and checks your code against it. The emitted JavaScript gets nothing from this file.
.d.ts means "declaration only." Your editor reads it through the same TypeScript language service that gives you red squiggles and hover types. That is why installing types for a package can make autocomplete appear immediately without changing the JavaScript package itself.
Where package types come from
When you import a package, TypeScript looks for its types while resolving the package. In practice, package types usually come from one of three places:
- The package bundles its own
.d.tsfiles and points to them from package metadata. - A separate
@types/*package supplies declarations from DefinitelyTyped. - Your project adds a local
.d.tsfile for a script or package TypeScript cannot otherwise describe.
A package with bundled types usually advertises them in package.json:
Example package metadata
node_modules/date-tools/package.json
{
"name": "date-tools",
"main": "dist/index.js",
"types": "dist/index.d.ts"
}That "types" field is a signpost. It says: the JavaScript entry is dist/index.js, and the declaration entry is dist/index.d.ts. Some packages use "typings" instead; it means the same job.
If a package is written in TypeScript, these declarations are often produced by its build. If a package is written in JavaScript, the author can still write declarations by hand and ship them next to the JavaScript. Either way, consumers get type checking without needing the source code.
@types packages and DefinitelyTyped
Some JavaScript packages do not ship their own declarations. When TypeScript cannot find types, the diagnostic points you at the usual fix:
Tool transcript: npx tsc --noEmit in a throwaway project with an untyped package
src/report.ts(1,29): error TS7016: Could not find a declaration file for module 'legacy-charts'. '/project/node_modules/legacy-charts/index.js' implicitly has an 'any' type.
Try `npm i --save-dev @types/legacy-charts` if it exists or add a new declaration (.d.ts) file containing `declare module 'legacy-charts';`That @types/legacy-charts name is not magic inside your app. It is an npm package containing .d.ts files. Most @types/* packages are generated from DefinitelyTyped, the community-maintained repository of declarations for JavaScript libraries that do not bundle their own types.
The normal workflow is:
npm install -D @types/legacy-charts
npx tsc --noEmitThe -D matters for the same reason TypeScript itself is a dev dependency: declarations help you build and check the project, but they are not runtime code your production program loads.
Scoped package names get flattened in @types: a package named @acme/charts would use @types/acme__charts. You do not need to memorize that rule; the TS7016 diagnostic and package docs usually tell you the right name.
Writing your first local declaration
If no bundled types and no @types/* package exist, add a local declaration file and include it in your project. For a global legacy script, the earlier types/legacy-members.d.ts file is enough as long as your tsconfig.json includes the types folder.
For an imported JavaScript package, the declaration names the module:
A static declaration-file example — read-only here because it describes an installed package's module rather than runnable playground code.
declare module "legacy-charts" {
export type ChartPoint = {
label: string;
value: number;
};
export type ChartOptions = {
title: string;
points: ChartPoint[];
};
export function renderChart(options: ChartOptions): string;
}Then application code can import from "legacy-charts" and the checker will use your declaration as the package's public shape. You should keep the declaration as small as possible at first: describe only the functions and values your project actually uses, then expand it when real code needs more surface.
Avoid the tempting shortcut from the TS7016 message:
declare module "legacy-charts";That silences the missing-types error by giving the module an unhelpfully loose shape. It gets you moving, but it gives up most of the safety you were trying to add. A small precise declaration is better than a giant vague one.
When the types lie
Declarations are promises. Like annotations, they can be wrong. TypeScript checks your code against the .d.ts file, not against the JavaScript implementation hiding behind it.
This is the version-skew risk: the JavaScript package and its declaration package must describe the same API. Bundled types reduce that risk because code and declarations ship together. Separate @types/* packages can lag behind, jump ahead, or model behavior imperfectly.
When you see a package-type mismatch, debug it like a boundary problem:
- Check the runtime package version.
- Check the
@types/*package version, if one exists. - Prefer bundled types when the package provides them.
- Read the declaration file when a hover looks suspicious.
- Patch locally with a narrow declaration only when you understand the real runtime shape.
Do not treat declarations as runtime validation. They make unknown JavaScript visible to the checker, but they do not inspect values while the program runs. Section 13 will return to that runtime boundary directly.
Next lesson opens the compiler itself just far enough to see the major stages. You now know what extra files the checker may read; 9.4 explains what it does with all those files once they are in the program.
Checkpoint
Answer all three to mark this lesson complete