TypeScript Declaration Files (.d.ts) Explained
A .d.ts file describes a library's types without its implementation. How TypeScript declaration files work, and how to write one for an untyped JS package.
A declaration file (.d.ts) describes the shape of a library’s types — its functions, classes, and objects — without containing any actual implementation. It’s how TypeScript type-checks code that calls into plain JavaScript: the .js file has the runtime logic, and a matching .d.ts file tells the compiler what types to expect, so you get autocomplete and type errors for a library that was never written in TypeScript at all.
Why declaration files exist
TypeScript compiles down to plain JavaScript, and type information doesn’t survive that compilation — it’s erased entirely, since JavaScript has no concept of static types at runtime. That erasure is fine for code written in TypeScript, because the compiler checks it before erasing anything. It’s a problem for consuming any library that wasn’t written in TypeScript, or whose compiled output has already had its types stripped: without a separate description of that library’s shape, TypeScript has nothing to check your calls against, and every import resolves to any.
Declaration files close that gap. They contain only type declarations — interface, type, declare function, declare class — with export statements mirroring the library’s actual exports, but zero executable code. TypeScript reads the .d.ts for type information and lets the bundler or runtime load the real .js for behavior; the two are consulted independently.
What a declaration file looks like
A minimal example for a small utility library:
// math-utils.d.ts
export function clamp(value: number, min: number, max: number): number;
export function lerp(start: number, end: number, t: number): number;
export interface Vector2 {
x: number;
y: number;
}
Nothing here has a function body — clamp isn’t implemented, just described. Consuming code imports from math-utils as normal, and TypeScript resolves the types from the .d.ts while the actual clamp logic runs from math-utils.js at runtime, entirely separate files working together.
Where declaration files come from
Most of the time, you don’t write these by hand:
- Bundled with the package. Many npm packages written in TypeScript ship a generated
.d.tsalongside their compiled JavaScript, pointed to by thetypes(ortypesVersions) field inpackage.json. TypeScript finds and uses these automatically — no configuration required. - DefinitelyTyped (
@types/*). For popular JavaScript-only packages that don’t ship their own types, the community maintains type definitions separately under the@typesscope on npm — installing@types/lodashalongsidelodashgives you full typing for a library that itself contains no type information. This is by far the most common way declaration files reach a project; see our guide to npm, pnpm, and Yarn for how these get resolved as ordinary dependencies. - Generated from your own source. Setting
"declaration": trueintsconfig.jsonmakes the compiler emit a.d.tsfile for every.tsfile it compiles, which is how a TypeScript library publishes types for its own consumers without hand-writing anything. - Written by hand. For a JavaScript-only package with no bundled types and no
@typespackage, or for describing global values injected by a script tag, an ambient value, or a non-JS asset import (like.svgor.css), a hand-written.d.tsis the only option.
Ambient declarations and declare global
Declaration files can also describe things that exist without a normal import — a global value attached by a <script> tag, a Node.js global, or a property some other script added to window. These use declare at the top level:
// globals.d.ts
declare const APP_VERSION: string;
declare global {
interface Window {
analytics: { track(event: string): void };
}
}
declare global is a form of declaration merging — it merges new members into the existing global Window interface rather than replacing it, which is exactly the mechanism that lets multiple libraries each augment the same global type without conflicting.
A file containing only declare module "some-untyped-package"; (no body) is the fastest way to silence “could not find a declaration file” for a package you don’t want to fully type yet — it tells TypeScript to treat every import from that module as any, trading safety for an unblocked build.
Typing non-JS imports
Bundlers like Vite and webpack let you import things that aren’t JavaScript at all — an SVG as a component, a CSS module, a JSON file, raw text. TypeScript doesn’t know what to do with those extensions by default, so projects that import them typically ship a small hand-written declaration file that tells the compiler what type each import produces:
// assets.d.ts
declare module "*.svg" {
const src: string;
export default src;
}
declare module "*.module.css" {
const classes: Record<string, string>;
export default classes;
}
This is the same declare module syntax used for untyped packages, just with a wildcard pattern instead of a package name — TypeScript matches it against the actual import specifier at each call site.
How TypeScript finds declaration files
Resolution follows a predictable order: TypeScript first checks whether the imported module is a local .ts or .tsx file, then looks for a bundled .d.ts referenced by that package’s types field, then checks node_modules/@types/<package-name>, and only falls back to any if none of those exist (or if noImplicitAny is off, silently). Any hand-written .d.ts files in your own project are picked up automatically as long as they’re included by your tsconfig.json’s include pattern — no explicit import needed, since ambient declarations are global to the compilation by design.
Regular .ts files vs .d.ts files
.ts files | .d.ts files | |
|---|---|---|
| Contains | Implementation + types | Types only, no implementation |
| Compiles to | JavaScript output | Nothing — erased entirely |
| Purpose | Runtime logic | Type information for the compiler |
| Typical origin | Code you write and run | Bundled with a package, from @types, or generated |
The takeaway
A .d.ts file is pure type information with no runtime behavior — it’s how TypeScript type-checks JavaScript it never compiled itself, whether that’s a package from @types, output generated from your own library, or a hand-written description of a global value. Most projects never need to write one by hand, but understanding what they are makes “cannot find type declarations for module X” a five-second fix instead of a mystery.
Keep reading
Takina · · 4 min read Structural Typing vs Nominal Typing in TypeScript
Structural typing checks shape, not name — TypeScript treats two differently-named types as compatible if their members match, unlike nominal systems.
Takina · · 5 min read TypeScript Variance Explained
Variance decides when TypeScript accepts Array<Dog> where Array<Animal> is expected — covariant, contravariant, or invariant, explained with examples.
Takina · · 5 min read Zod vs Yup: TypeScript Schema Validation Compared
Zod infers static TypeScript types directly from its schemas; Yup was built for JavaScript form validation first. How the two approaches differ.