Declaration Files (.d.ts)
A file that describes the shape of existing JavaScript code, without containing any actual implementation, so TypeScript can check code that uses it.
What is it?
Not all code you use is written in TypeScript. A huge amount of the JavaScript ecosystem — older libraries, many npm packages, code your team wrote years ago — is plain .js, with no type annotations at all. If you import one of those into a TypeScript project, the compiler has no way to know what shape its functions, objects, and exports actually have, so every value coming from it effectively becomes any.
A declaration file, ending in .d.ts, solves this by describing the shape of that JavaScript — every exported function's signature, every exported object's structure — without containing any real logic at all. It's pure type information, describing what's already there, so the compiler can check code that uses that library the same way it would check a fully-typed TypeScript module.
You'll encounter these in two common ways: many popular libraries ship their own .d.ts files describing themselves, and for libraries that don't, the community-maintained DefinitelyTyped project publishes separate @types/<package-name> packages containing hand-written declaration files for them.
Explain like I'm 10
A declaration file is like an appliance's spec sheet, sold separately from the appliance itself. The spec sheet tells you exactly what buttons exist, what each one accepts, and what it outputs — without containing any of the actual wiring inside. TypeScript reads the spec sheet to check that you're using the appliance correctly, even though the real appliance was built by someone else in a completely different factory.
Examples
Describing an existing JavaScript module
// mathUtils.js — a plain JavaScript file, no types
function double(x) {
return x * 2;
}
module.exports = { double };This is ordinary, untyped JavaScript. Imported directly into a TypeScript project with no declaration file, double would be typed as any, and TypeScript couldn't catch a mistaken call like double("5").
A matching declaration file
// mathUtils.d.ts — describes mathUtils.js, contains no implementation
declare function double(x: number): number;
export { double };Placing this file alongside mathUtils.js gives TypeScript enough information to type-check every import of mathUtils, as if it had been written in TypeScript from the start — even though the actual logic still lives entirely in the .js file.
How it works
A .d.ts file uses the declare keyword to describe things that exist elsewhere at runtime — functions, variables, classes, whole modules — without providing their actual implementation. When you import from a .js file that has a matching .d.ts file nearby (or a separately installed @types/<package> package), the TypeScript compiler reads the declaration file to learn the shapes involved, and checks all your usage against those shapes. At compile time, the .d.ts file is purely informational for the type checker; it produces no JavaScript output of its own, and the actual code that runs is still whatever is in the real .js file.
Why does it exist?
TypeScript's whole benefit — catching type mismatches before code runs — would stop at the boundary of any untyped JavaScript dependency, forcing every import from such a library to fall back to any and lose all checking. Declaration files let type information be attached to existing JavaScript after the fact, without rewriting that JavaScript, so TypeScript's checking can extend across the entire dependency graph, not just the code written in TypeScript directly.
When to use it
Write a declaration file when you're using a JavaScript library that has no types of its own and no @types/ package available for it — you write a .d.ts describing just enough of its shape for your code to be checked against it. You'll also encounter (and occasionally need to read or tweak) generated declaration files when publishing your own TypeScript library, so consumers get type checking without needing your original source.
When not to use it
Don't hand-write a declaration file for a library that already ships its own types or has a well-maintained @types/ package — check first, since duplicating or conflicting with an existing declaration causes confusing errors. Also avoid writing one just to silence errors on code you actually intend to migrate to TypeScript directly — converting the source is usually better long-term than perpetually describing it from the outside.
Common mistakes
Writing actual implementation logic inside a
.d.tsfile — declaration files are type-only, and any executable code inside them is not what actually runs.Not realizing a library already ships its own types (check its
package.jsonfor atypesortypingsfield) before writing or installing a redundant declaration.Letting a hand-written
.d.tsdrift out of sync with the real JavaScript it describes, so TypeScript ends up confidently checking against an inaccurate shape.
Practice exercises
- Easy:
Given a plain JavaScript function
function add(a, b) { return a + b; }, write a.d.tsdeclaration describing it as taking two numbers and returning a number. - Medium:
Look up (or imagine) a small npm package with no built-in types, and write out what installing its
@types/package via npm would look like, and why it would let you import the package with full type checking. - Hard:
Write a declaration file describing a small JavaScript module that exports an object with a nested method (e.g.
logger.info(msg)andlogger.error(msg)), using adeclare moduleordeclare namespacestructure.
Interview questions
What is a `.d.ts` file?
A declaration file that describes the type shape of existing JavaScript code — functions, variables, classes, modules — without containing any actual implementation, so TypeScript can type-check code that uses it.
What is DefinitelyTyped, and what are `@types/` packages?
DefinitelyTyped is a community-maintained repository of declaration files for JavaScript libraries that don't ship their own types; those declarations are published as separate @types/<package-name> npm packages you can install alongside the library.
Does a `.d.ts` file produce any JavaScript when compiled?
No. It's purely type information for the compiler — it contains no runtime logic and produces no JavaScript output of its own.
What does `declare module "some-module"` do, and when would you reach for it?
It creates an ambient module declaration — a block describing the shape of an entire module by name, used for a library with no .d.ts of its own and no file structure to attach declarations to directly. Anything declared inside it becomes the type of whatever gets imported using that exact module specifier.
How would you give TypeScript a type for `import logo from "./logo.png"` in a project that doesn't natively understand image imports?
A wildcard ambient module declaration: declare module "*.png" { const src: string; export default src; }. The * wildcard matches any specifier ending in .png, and the declaration says a default import from one resolves to a string (the asset URL a bundler produces at build time).
What makes a `.d.ts` file a global (ambient) script versus a module?
Presence of a top-level import or export statement. A file with at least one of those is treated as a module — its declarations are scoped to that file and must be explicitly imported elsewhere. A file with neither is a global script, and everything it declares becomes available ambiently, project-wide, with no import needed.
You added a global-style `declare const API_URL: string;` inside a file that also has an `export` statement elsewhere, and now other files can't see `API_URL` without importing it. Why?
Adding any top-level export (or import) anywhere in the file turns the whole file into a module, which scopes every declaration inside it to that module instead of leaving it ambient. To keep API_URL truly global, it needs to live in a file with no top-level import/export, or be wrapped in a declare global { ... } block.
How do you add ambient/global declarations from inside a file that's otherwise a module?
Wrap them in a declare global { ... } block. This lets a single file be a module for its own exports while still contributing declarations to the global scope — commonly used to augment things like the global Window interface.
How does TypeScript find the `.d.ts` files for a third-party package that doesn't ship its own types?
It looks for a separately installed @types/<package-name> package under node_modules/@types, installed like any other dependency (e.g. npm install --save-dev @types/lodash). TypeScript automatically includes everything under @types by default, without needing to import it directly.
If a package ships its own types, how does TypeScript know which file to load?
It checks the package's package.json for a types (or the older typings) field pointing at the entry .d.ts file; if that's absent, TypeScript falls back to looking for a .d.ts file matching the package's main entry point by name.
You install a package whose `@types/<name>` version doesn't match the actual installed package version — what problems can that cause?
The declaration file can describe a signature, option, or export that either doesn't exist in the version you actually installed, or is missing one that does — causing either false compile errors on perfectly fine code, or, worse, code that compiles but crashes at runtime because the real API doesn't match what the mismatched types promised.
What is a triple-slash reference directive, and when is it still needed?
A special comment, valid only at the top of a file — /// <reference path="..." /> or /// <reference types="..." /> — that tells the compiler to include another declaration file or an @types package before checking the current file. It's used to wire together older-style global .d.ts files that don't use import/export, or to pull in ambient types not otherwise picked up automatically.
What does `export = ` mean inside a declaration file?
It describes a CommonJS-style module whose entire export is a single value, matching module.exports = something in the real JavaScript. It lets TypeScript correctly type an import written as import foo = require("foo") (or, with esModuleInterop, a default import), which plain export default syntax can't accurately represent for that shape.
What's the difference between `declare namespace Foo { ... }` and `declare module "foo" { ... }`?
declare module "foo" describes an importable module resolved by that exact string specifier, accessed via import. declare namespace Foo instead declares an ambient global grouping (Foo.Bar, Foo.Baz) accessed directly by name with no import — used for older global-script-style libraries rather than modern module imports.
How would you describe a JS module that exports an object with nested methods, like `logger.info(msg)` and `logger.error(msg)`?
An interface describing the shape, exported as the module's export: interface Logger { info(msg: string): void; error(msg: string): void; } declare const logger: Logger; export default logger; — mirroring however the real module actually structures and exports that object.
What does the `skipLibCheck` compiler option do, and why do many projects enable it?
It skips type-checking the contents of all .d.ts files (your own and dependencies'), only reading them for the shapes they describe. It's commonly enabled because a project has no control over errors inside third-party declaration files, and checking every dependency's types on every build is slow and pointless if you can't fix them anyway.
A hand-written `.d.ts` for an internal JS module has fallen out of sync with the real implementation — what's the actual risk, versus a `.ts` file being wrong?
In a real .ts file, the compiler checks the implementation against its own declared types, so a mismatch is usually caught immediately. A hand-written .d.ts has no such cross-check against the JS it describes — the compiler simply trusts it, so callers get confidently wrong type information that only surfaces as a runtime bug.
How does `declare function double(x: number): number;` differ from writing that same signature with a real function body in a `.ts` file?
The declare version has no function body and isn't allowed one — it's a pure assertion that a function with this exact signature exists somewhere at runtime, typically in a paired .js file. The plain .ts version both declares the type and provides the actual implementation that gets compiled to real JavaScript.
Why can a `.d.ts` file for a library be written and published independently from that library's actual source code?
Declaration files carry no logic — they're a pure description of shape the compiler only needs at type-checking time, never at runtime. That separation is exactly what lets a different party, like DefinitelyTyped contributors, author and maintain accurate types for a library's public API without needing write access to its real source.
If a library ships its own `.d.ts` files and an outdated `@types/<name>` package also exists for it, which does TypeScript actually use?
TypeScript prefers the types the package itself ships (via its package.json types field) over a separate @types/<name> package. Having both installed is usually just a stale leftover — it's worth removing the redundant @types package to avoid confusion or, more rarely, genuine declaration conflicts.
If a `.d.ts` file's `declare function double(x: number): number;` doesn't match what the real `mathUtils.js` actually does at runtime (say it really returns a string), what will TypeScript report?
Nothing — TypeScript never inspects the real runtime behavior of mathUtils.js; it only ever sees and trusts the declaration file. Callers get type-checked against the promised number return type, so this failure mode is entirely silent at compile time and only shows up as a runtime mismatch.
How does an `@types` package differ from a library shipping types in its own package alongside its code?
An @types package is maintained completely separately (usually by DefinitelyTyped contributors, not the library's authors) and installed as its own dependency, so it can drift from the real library's version. Types shipped directly inside the library's own package are maintained by the same people writing the implementation and versioned together with it, so they're generally more likely to stay accurate.