Right, so you’ve decided to step into the world of TypeScript’s ambient declarations. Good for you. This is where we stop just using types and start telling TypeScript about types that exist elsewhere. Think of it as being a translator for a system that doesn’t speak TypeScript natively—like a glob of vanilla JavaScript, a library loaded from a <script> tag, or some magic your build process injects.

We use the declare keyword to shout into the void, “Hey TypeScript compiler! Trust me on this. This thing exists, and this is its shape. Now stop complaining about it.” It’s a promise, and if you break that promise at runtime, well, that’s on you. The compiler will have already packed up and gone home.

The Humble declare var

This is your most basic building block. You use it to tell TypeScript about a global variable that exists in the great wide somewhere (usually the global scope, window).

Let’s say you have a legacy site that has a global __version string floating around, set by some other script. TypeScript, rightly so, will yell at you for using an undeclared variable.

// Try to use it, and TS will have a fit:
console.log(`App version: ${__version}`); // Cannot find name '__version'.(2304)

// So we declare its existence and type:
declare var __version: string;

// Now the compiler is happy and trusts you to not be a maniac.
console.log(`App version: ${__version}`); // All good!

A quick but crucial note: declare var only declares that a variable exists. It doesn’t create it. If you don’t have a var __version = "1.2.3"; in your actual JavaScript somewhere, this will compile perfectly and then explode into undefined at runtime. The compiler takes you at your word, so your word had better be good.

Describing Behavior with declare function

Next up, telling TypeScript about functions that live outside its jurisdiction. This is how you describe the shape of a function—its parameters and return type—without providing an implementation. It’s just a type signature with a declare stuck on the front.

Imagine you have a utility function from a third-party analytics script:

// The actual JS function might be: `function trackEvent(name, props) { ... }`
// We tell TS about its contract:
declare function trackEvent(eventName: string, eventProperties?: Record<string, any>): void;

// Now we can use it safely:
trackEvent("page_view", { path: "/home" });
trackEvent("click"); // `eventProperties` is optional, so this is fine

// And TS will catch our mistakes:
trackEvent(123); // Error: Argument of type 'number' is not assignable to 'string'.(2345)

This is infinitely more useful than just declaring a variable of type Function. That would tell TypeScript “a function exists here,” but declare function tells it exactly what kind of function exists here, enabling full type-checking on the calls.

Modeling Complex Objects with declare class

When you need to describe a class that’s defined elsewhere (a common pattern with older JS libraries), you use declare class. This lets you define the structure of the class—its constructor, methods, and properties—without the actual implementation code.

Here’s the thing TypeScript designers got right: a declare class creates a type and a value. This is different from an interface, which only creates a type. That’s why you can use declare class to model something you can actually new up.

Let’s say you have a Widget class from a library loaded via a <script> tag.

// The actual JS might look like:
// function Widget(id) { this.id = id; }
// Widget.prototype.update = function(data) { ... };

// Our ambient declaration for it:
declare class Widget {
  id: string;
  constructor(id: string); // declaring the constructor is key
  update(data: Record<string, any>): void;
  isEnabled: boolean; // a property
}

// Now we can use it with full type safety:
const myWidget = new Widget("abc123"); // TypeScript knows `myWidget` is an instance of `Widget`
myWidget.update({ value: 42 });
myWidget.isEnabled = true;

// And it will protect us from nonsense:
const badWidget = new Widget(100); // Error: Argument of type 'number' is not assignable to 'string'.(2345)
myWidget.destroy(); // Error: Property 'destroy' does not exist on type 'Widget'.(2339)

The beauty here is that we’ve fully typed a class we didn’t write, getting autocomplete and error checking as if it were a native TypeScript class. The rough edge? You’re on the hook for keeping this declaration accurate. If the library updates and changes the update method signature, your types are a lie until you fix your declaration.