Skip to Content
Course content

160: Structure of .d.ts Files

Click on the "Edit" button in the top corner of the screen to edit your slide content.

By now, you've spent most of your time writing TypeScript in .ts files where the logic and the types live together. But eventually, you'll run into a situation where you're using a JavaScript library that doesn't have types, or you're building a library for others to use, and you need to provide a .d.ts file. These declaration files are essentially "type-only" versions of your code. The mistake I see most often—and one I made early on—is treating a .d.ts file like a regular TypeScript file where you just list your interfaces.

The Global Pollution Trap

When you first start writing a declaration file, the instinctive approach is to just use the declare keyword for everything. Let's say you're adding types for a legacy JS analytics script called tiny-tracker that attaches a global object to the window. You might write something like this:

// tiny-tracker.d.ts
declare var TinyTracker: {
    trackEvent: (name: string, props: object) => void;
    setUserId: (id: string) => void;
};

On the surface, this works. You'll see that TinyTracker is available everywhere in your project without an import statement. But here is where it breaks: you've just polluted the global namespace. If another library happens to define a TinyTracker, or if you accidentally name a local variable the same thing, you're headed for a collision. More importantly, this approach fails the moment you move from a simple script tag to a modern module system (ESM). If you try to import { TinyTracker } from 'tiny-tracker', TypeScript will complain because your .d.ts file didn't actually define a module; it just told the compiler, "Trust me, this thing exists globally."

Mapping Types to Modules

The professional way to handle this is to explicitly wrap your declarations in a module block. This tells TypeScript exactly which package or file these types belong to. Instead of letting the types float in the global void, you bind them to a module name that matches your import statement.

// tiny-tracker.d.ts
declare module 'tiny-tracker' {
    export function trackEvent(name: string, props: object): void;
    export function setUserId(id: string): void;
}

I prefer this because it mirrors how we actually write code. When you use declare module 'package-name', you're creating an "ambient module." This allows you to keep your type definitions in a separate file while still enabling the import { ... } from 'package-name' syntax. It's a clean separation. The compiler no longer assumes trackEvent is a global variable; it knows it's a named export from a specific module.

The Subtle Shift: Ambient vs. Local Declarations

There is a nuance here that often trips people up. If your .d.ts file contains a top-level import or export statement, TypeScript treats the entire file as a module. If it doesn't, it treats the file as a global script. This is a huge distinction.

If you write a .d.ts file with just interface User { name: string } (no exports), that User type is available everywhere in your project. That sounds convenient, but it's actually a maintenance nightmare in large codebases. You end up with "magic types" that appear out of nowhere, making it impossible to track where a type is actually defined. I always recommend using the module approach—even for internal types—because explicit imports are always better than implicit globals. It makes the dependency graph clear and prevents the "where did this type come from?" scavenger hunt during a midnight debugging session.




📋 Practical Task

Refactoring Global Types for 'fast-cache'

You have been handed a legacy fast-cache.d.ts file that was written using the "global" approach. This is causing naming collisions in a large project. Your task is to convert this global declaration into a proper module declaration so it can be imported correctly.

Current broken file (fast-cache.d.ts):

declare var FastCache: {
    get: (key: string) => any;
    set: (key: string, value: any) => void;
    clear: () => void;
};

Your Goal: Rewrite the content of fast-cache.d.ts so that the types are contained within a module named 'fast-cache' and the functions are exported individually. This will allow the team to use import { get, set, clear } from 'fast-cache' instead of relying on a global FastCache object.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.