Declaration Files (.d.ts): Types for Untyped Code

Learn what TypeScript declaration files (.d.ts) are, how ambient declarations and the declare keyword work, and why you consume @types packages far more often than you write them.

Published September 20, 20269 min readBy ACY Partner Indonesia
Declaration Files dot d dot t s cover with declare module code chip
300 × 250Ad Space AvailablePlace your ad here

You install a package, write a line of code that uses it, and TypeScript instantly knows the shape of everything it returns. It autocompletes the method names. It warns you when you pass a number where a string belongs. That feels like magic, especially when the package itself was written in plain JavaScript with no types at all. So where did all that type information come from?

The answer is a quiet little file type with a strange name: the declaration file, ending in .d.ts. It is one of the most important pieces of the TypeScript world, and yet most developers go years without ever writing one by hand. In this article we will unpack what these files are, the idea of “ambient” declarations, the declare keyword, and the large community project that supplies types for thousands of JavaScript libraries. By the end, the magic will feel a lot less mysterious.

The problem declaration files solve

TypeScript and JavaScript have a peculiar relationship. JavaScript is the language that actually runs in browsers and on servers. TypeScript is a layer on top that adds type information, and that information is checked while you write code and then erased before the code runs. The browser never sees a type.

This creates a gap. A huge amount of code in the world is written in plain JavaScript. Older libraries, internal scripts, packages that were published before TypeScript became popular: none of them carry type information. When you try to use that code from a TypeScript project, TypeScript has no idea what any of it is. It does not know that a function expects two arguments, or that an object has a name property. As far as the type checker is concerned, the code is a black box.

A declaration file is how you describe that black box from the outside. It is a file that contains only type information and no runnable logic. Its whole job is to tell TypeScript, “here is the shape of this JavaScript code” so the type checker can do its work.

Types only, no behavior

A .d.ts file never contains the actual implementation of anything. It holds type signatures, interface shapes, and function declarations. Think of it as a label on a sealed box: it tells you what is inside without being the contents itself.

What “ambient” means

When you read about declaration files, you will keep running into the word ambient. It sounds technical, but the idea is simple.

Normally in TypeScript, a type exists because you wrote some code that creates it. You define a function, and TypeScript infers its type from the function body. The type and the real value live together.

An ambient declaration is different. It describes a value or type that exists somewhere else, in code TypeScript cannot see or check. You are not creating anything; you are announcing that something already exists in the surrounding environment. The word “ambient” captures that nicely, like describing the temperature of a room you are standing in but did not build.

A classic example is a global variable that a script tag loaded onto the page. The variable is genuinely there at runtime, but there is no TypeScript code that produces it. So you write an ambient declaration to tell the compiler about it.

The declare keyword

The tool you use to write ambient declarations is the declare keyword. It is a promise to TypeScript: “trust me, this thing exists at runtime, even though you cannot see the code that makes it.”

Imagine a third-party analytics script adds a global function called trackEvent to the page. In your TypeScript code you want to call it without errors. You can declare it:

// analytics.d.ts
declare function trackEvent(name: string, value: number): void;

Now anywhere in your project you can call trackEvent("signup", 1) and TypeScript will check the arguments against that signature, even though the real function lives in some external script. Notice there is no function body. The declare keyword means you are only describing the shape, not providing the code.

You can declare more than functions. Variables, classes, and entire modules can all be declared:

// globals.d.ts
declare const APP_VERSION: string;

declare namespace AppConfig {
  const apiUrl: string;
  const timeout: number;
}

A promise you must keep

declare tells TypeScript to assume something exists, but it does not make it exist. If you declare a function that is not actually present at runtime, your code will compile cleanly and then crash when it runs. The declaration is a description, not a guarantee. Make sure what you declare truly is loaded.

Describing a whole module

Many JavaScript libraries are not global variables; they are modules you import. To describe one of those, you use a slightly larger pattern called a module declaration.

Suppose there is a plain-JavaScript package named simple-logger with no types. You import it like this:

import { log } from "simple-logger";

Without types, TypeScript complains that it cannot find the module. You can write a declaration file that describes its public surface:

// simple-logger.d.ts
declare module "simple-logger" {
  export function log(message: string): void;
  export function warn(message: string): void;
  export const version: string;
}

Now TypeScript treats simple-logger as if it were fully typed. You get autocomplete on log and warn, and a red squiggle if you call them wrong. The actual JavaScript inside the package never changed; you simply described it from the outside.

This is the core mental model worth holding onto:

  Plain JavaScript library          Declaration file (.d.ts)
  ┌──────────────────────┐          ┌──────────────────────┐
  │ real code that runs  │  ◄────►  │ types that describe   │
  │ (no type info)       │          │ that code (no runtime)│
  └──────────────────────┘          └──────────────────────┘
            │                                  │
            └──────────► TypeScript ◄──────────┘
                  sees the runtime behavior
                  AND understands the types

DefinitelyTyped and @types

Here is the part that explains the everyday magic. If declaration files describe untyped libraries, who writes the declarations for the thousands of popular JavaScript packages out there? You could write your own for each one, but that would be an enormous, repetitive job.

The community solved this with a single, gigantic, open project called DefinitelyTyped. It is a public repository where volunteers contribute high-quality declaration files for popular JavaScript libraries that do not ship their own types. When someone writes the declarations for a library, everyone benefits.

These declarations are published to the npm registry under a special scope named @types. The naming follows a simple pattern. If a library is called lodash, its community-maintained types live in a package called @types/lodash. If it is called express, the types are in @types/express.

npm install lodash
npm install --save-dev @types/lodash

The first line installs the real library, the code that runs. The second line installs the matching declaration files, the types that describe it. TypeScript automatically looks inside @types packages and applies them. You import lodash normally and suddenly everything is typed, even though lodash itself was written in plain JavaScript.

Check for bundled types first

Many modern libraries now ship their own .d.ts files inside the package, so you do not need a separate @types install at all. A good habit: try importing the library first. If TypeScript already understands it, the types are bundled. Only reach for @types/... when the editor complains that it cannot find a declaration.

Why you consume far more than you write

Put the pieces together and a comfortable truth emerges: as a working developer, you will read and rely on declaration files constantly, but you will write them rarely.

Every time you install a typed library, you are consuming declaration files. Every autocomplete suggestion, every type error on a library function, every hover tooltip that shows you a function signature, all of it flows from .d.ts files that someone else already wrote, either the library author or a DefinitelyTyped contributor.

The situations where you actually write one are narrow:

Situation Do you write a .d.ts?
Using a typed library No, types come with it
Using a library covered by @types No, just install @types
A global from a script tag Sometimes, a small declaration
An old internal JS library nobody typed Occasionally, if you must
Importing non-code assets (images, etc.) Sometimes, a tiny module declaration

For most projects, the only hand-written declarations are a few lines telling TypeScript about a global or an unusual import. The heavy lifting is already done for you.

Modern TypeScript writes them for you

When you build your own TypeScript library and compile it, the compiler can automatically generate .d.ts files from your source. Your code already has types, so TypeScript just extracts them into declaration files for your users. This is how a well-published TypeScript package ships its types: nobody writes the declarations by hand, the compiler does it.

A quick reality check on accuracy

One honest caveat is worth stating. A declaration file is only as correct as the person who wrote it. Because it is a separate description of code that lives elsewhere, the two can drift apart. A library might add a new option that the declaration does not mention yet, or a community type might describe an older version.

When that happens, TypeScript still trusts the declaration, because that is all it has to go on. The code may pass the type check and still misbehave. This is rare with popular libraries, which have many eyes on their types, but it is the reason declarations are a description and not a promise. Treat surprising type errors against a library as a clue to check whether the types match the version you installed.

Summary

Declaration files, the ones ending in .d.ts, exist to give TypeScript a vocabulary for code it cannot see: plain JavaScript that carries no type information of its own. They contain types and nothing runnable.

The key ideas come together neatly. An ambient declaration describes something that already exists in the surrounding environment rather than creating it. The declare keyword is how you write those descriptions, whether for a global function, a variable, or an entire module. And the DefinitelyTyped project, published as @types packages on npm, supplies ready-made declarations for thousands of untyped libraries, which is why installing @types/lodash instantly makes lodash feel fully typed.

The practical takeaway is freeing: you do not need to master writing declaration files to use TypeScript well. You will consume them every day through the libraries you install, and you will only occasionally write a handful of lines to describe a global or an odd import. Knowing what .d.ts files are and where the types come from is enough to make the whole system feel transparent instead of magical.

Tags:typescriptdeclaration-filesd-tstypesdefinitelytyped
728 × 90Ad Space AvailablePlace your ad here

Related Articles

See All Articles

You Might Also Like

Frontend best practices overview cover
Frontend / Fundamentals

Frontend Best Practices: An Overview

A friendly map of what good frontend really means — semantic HTML, clean CSS, unobtrusive JavaScript, responsive layouts, performance, and progressive enhancement, with pointers to go deeper.

Sep 20, 20268 min read
A Frontend Developer Roadmap cover with the path HTML to CSS to JavaScript
Frontend / Fundamentals

A Frontend Developer Roadmap: What to Learn, and in What Order

A calm, step-by-step learning path for beginners: HTML, CSS, JavaScript, developer tools, responsive and accessible design, a framework, TypeScript, and deployment, with the reasoning behind each step.

Sep 20, 202611 min read
Title card reading Styling and Interactivity over a dark blue ACY Partner background
Frontend / Fundamentals

How Styling and Interactivity Work

A beginner-friendly look at how CSS turns plain HTML into a designed layout, and how JavaScript adds behavior — the structure, style, behavior mental model for building web pages.

Sep 20, 20269 min read
Three front-end layers combining on one web page
Frontend / Fundamentals

How HTML, CSS, and JavaScript Work Together

A hands-on walkthrough for beginners: build one small button, give it structure with HTML, looks with CSS, and behavior with JavaScript, and watch the three layers cooperate on a real page.

Sep 20, 20268 min read
The Frontend Developer Toolkit cover with editor, browser, and CLI motifs
Frontend / Fundamentals

The Frontend Developer Toolkit

A friendly tour of the everyday tools a frontend developer relies on — code editor, browser and DevTools, terminal, version control, package managers, and a dev server — and what each one is actually for.

Sep 20, 202610 min read
Cover illustration for What Frontend Developers Do
Frontend / Fundamentals

What Frontend Developers Do: A Beginner's Guide to the Role

A clear, beginner-friendly look at what frontend developers actually do every day: turning designs into working interfaces, building reusable UI, and caring about responsiveness, accessibility, and speed.

Sep 20, 202610 min read