Skip to Content
Course content

50: Ambient Declarations

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

I was working on a legacy project the other day where the team had this old, proprietary analytics library—let's call it LegacyStats. It wasn't an npm package; it was just a 50KB JS file loaded via a <script> tag in the HTML head. It just dumps a global LegacyStats object onto the window.

The "Cannot find name" wall

Naturally, as soon as I tried to use it in a TypeScript file, the compiler lost its mind. I wrote this:

const report = LegacyStats.generateReport('q3_sales');
console.log(report);

Right away, I get the dreaded: Cannot find name 'LegacyStats'.

Now, if you run this code in the browser, it works perfectly. The library is there, the function exists. But TypeScript is a pessimist. If it can't see a const, let, class, or an import statement that defines LegacyStats, it assumes it doesn't exist. It doesn't know about your HTML file or the random scripts you loaded in the browser.

Temptation of the any cast

My first instinct when I'm in a rush is usually to just cheat. I can tell TypeScript to stop complaining by casting the window object to any:

const report = (window as any).LegacyStats.generateReport('q3_sales');

The error goes away. Great. But now I have no autocomplete. I have no idea if generateReport takes a string, a number, or a configuration object. I'm basically writing JavaScript again, which defeats the whole reason we're using TypeScript. I want the safety, but I can't actually provide the implementation because the code lives in that external JS file.

Defining the ghost in the machine

This is where "ambient declarations" come in. I need a way to tell TypeScript: "Hey, I'm not defining this variable right now, but I promise you it will exist when the code actually runs."

The keyword for this is declare. I tried adding this to the top of my file:

declare const LegacyStats: any;

const report = LegacyStats.generateReport('q3_sales');

The error disappears, and it's slightly cleaner than the window as any hack. But it's still any. I know the library has a generateReport method that takes a string and returns an object with a total and a count. Let's actually describe it.

interface StatsReport {
    total: number;
    count: number;
}

interface LegacyStatsLib {
    generateReport(id: string): StatsReport;
    clearCache(): void;
}

declare const LegacyStats: LegacyStatsLib;

Now, when I type LegacyStats., my IDE actually suggests generateReport and clearCache. This is the magic of ambient declarations. I've described the shape of the code without actually providing the logic.

Moving to the .d.ts file

Putting these declare statements at the top of every file where I use the library would be a nightmare. It's repetitive and clutters the logic. I noticed that TypeScript treats files ending in .d.ts (declaration files) differently. They are intended specifically for these types of ambient descriptions.

I created a file called globals.d.ts and moved my declarations there:

// globals.d.ts
interface StatsReport {
    total: number;
    count: number;
}

interface LegacyStatsLib {
    generateReport(id: string): StatsReport;
    clearCache(): void;
}

declare const LegacyStats: LegacyStatsLib;

The moment I saved that file, the LegacyStats object became available globally across my entire project. I didn't have to import it, and I didn't have to declare it in my main .ts files. TypeScript just "knows" it's there. This is how the @types` packages work under the hood—they are essentially just massive collections of .d.ts files telling TypeScript how external JS libraries are shaped.




📋 Practical Task

Fixing the Missing Types for the Legacy Maps Library

You are working on a project that uses an old global mapping library called QuickMap. The library is loaded via a CDN in the HTML, so TypeScript is currently flagging every use of QuickMap as an error.

Your goal: Create an ambient declaration that allows the project to compile and provides full type safety for the library.

The QuickMap object has the following API:
1. A method init(elementId: string, apiKey: string): void
2. A method setZoom(level: number): void
3. A property currentCenter which is an object containing lat: number and lng: number.

Requirements:

  • Create a declarations.d.ts file.
  • Define an interface for the QuickMap object.
  • Use the declare keyword to make the QuickMap constant globally available.
  • In a separate main.ts file, call the init method and log the currentCenter.lat to the console to verify that the types are working.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.