Ambient Declarations in TypeScript

Ambient declarations use the declare keyword to tell TypeScript about values that exist at runtime but were not written in TypeScript. Learn how and when to use them.

6 min read

TypeScript ambient declarations tell the compiler: "This thing exists at runtime, but I am not going to implement it here. Trust me that it will be available when the code runs."

The keyword that signals this is declare. It appears before variable, function, class, namespace, module, and enum declarations. When TypeScript sees it, the compiler uses the type information for checking but skips the declaration during code generation, so no JavaScript is emitted.

Ambient declarations are the foundation of every declaration file. They are also useful inside regular TypeScript source files when you need to describe something that TypeScript cannot see.

Why You Need Ambient Declarations

TypeScript can type-check only the code it can see. If a value comes from a plain JavaScript file, a <script> tag in HTML, a browser built-in, or a Node.js global, TypeScript has no type information for it.

Ambient declarations fill that gap. They let you describe the types of things that are already present in the runtime environment.

Node.js injects a global object called process that TypeScript cannot see on its own. Referencing it in a plain TypeScript file without any type information produces a compiler error.

typescripttypescript
// Without an ambient declaration, TypeScript errors:
console.log(process.env.NODE_ENV);
//           ^ Cannot find name 'process'.

The process global is typed by an ambient declaration shipped with the @types/node package, which you install as a dev dependency alongside your project's other types. The snippet below is a simplified version of that declaration, showing only its most-used shape.

typescripttypescript
declare var process: {
  env: { [key: string]: string | undefined };
  platform: string;
};

Because this declaration exists, TypeScript knows process.env is an object with string keys, so process.env.NODE_ENV type-checks correctly once @types/node is installed.

Declaring Ambient Variables

Use declare with var, let, or const to tell TypeScript about a variable that exists in the global scope:

typescripttypescript
// The API_KEY global is injected by a build tool at runtime.
declare const API_KEY: string;
 
console.log(`Using API key: ${API_KEY}`);

The const modifier tells TypeScript the value will not be reassigned, which allows literal type inference. Use var or let instead when the underlying value is expected to change during the program's run, as shown below with a counter that increments over time.

typescripttypescript
declare var userCounter: number;
 
// This compiles because var allows reassignment.
userCounter += 1;

Declaring Ambient Functions

Use declare with a function signature to describe a function that exists globally, without writing a body for it. The example below tells TypeScript that a trackEvent function is available at runtime, so calls to it get checked like any other typed function.

typescripttypescript
declare function trackEvent(name: string, data?: Record<string, unknown>): void;
 
// TypeScript now checks calls to trackEvent:
trackEvent("page_view", { url: "/home" }); // OK
trackEvent(42);
//         ~~
//         Argument of type 'number' is not assignable to parameter of type 'string'.

The function body is never written in the declaration. The implementation lives elsewhere, in a separate JavaScript file, a third-party analytics script, or a browser API.

Function Overloads in Ambient Declarations

You can repeat a declare function signature with the same name to describe overloads, the same way you would overload a normal TypeScript function. Each signature narrows what argument types are valid for that call shape.

typescripttypescript
interface User {
  id: number;
  name: string;
}
 
declare function fetchUser(id: number): User;
declare function fetchUser(username: string): User;
 
const user1 = fetchUser(1);       // User
const user2 = fetchUser("alice"); // User

TypeScript picks the matching overload based on the argument type, so both calls above resolve to the same return type without any casting.

Declaring Ambient Classes

Use declare with a class body to describe a class constructor and its instance shape. This tells TypeScript what methods and properties an instance has, without providing any method bodies.

typescripttypescript
declare class Logger {
  constructor(prefix: string);
  log(message: string): void;
  warn(message: string): void;
}

This syntax describes both the static side, meaning the constructor, and the instance side, meaning the methods and properties available on a new instance. Calling a real method compiles, but referencing one that was never declared is rejected.

typescripttypescript
const logger = new Logger("[App]");
logger.log("Server started"); // OK
logger.error("Something");
//     ^ Property 'error' does not exist on type 'Logger'.

Declaring Ambient Namespaces

Use declare with a namespace block to group related types and values under one shared name, instead of declaring several unrelated globals.

typescripttypescript
declare namespace AppConfig {
  const version: string;
  const debug: boolean;
  interface FeatureFlags {
    darkMode: boolean;
    betaFeatures: boolean;
  }
}

The namespace bundles two values and one interface. Consumers reach each member through the namespace name, and the interface works as a normal type wherever it is referenced.

typescripttypescript
const v = AppConfig.version;     // string
const d = AppConfig.debug;       // boolean
 
const flags: AppConfig.FeatureFlags = {
  darkMode: true,
  betaFeatures: false,
};

Namespaces are useful when you have multiple related globals or when a library exposes its API through a nested object.

Namespaces can be nested for deeper organization:

typescripttypescript
declare namespace App {
  namespace UI {
    function showNotification(message: string): void;
  }
  namespace Data {
    function fetchConfig(): Promise<Record<string, unknown>>;
  }
}
 
App.UI.showNotification("Ready"); // OK

Ambient Declarations vs Export Declarations

Ambient declarations in a declaration file behave differently depending on whether the file is treated as a module or a script. The table below compares the two.

File typeTriggerVisibility
ScriptNo top-level import or exportDeclarations are global, no import needed
ModuleAt least one top-level import or exportDeclarations are local unless individually exported

A script-style declaration file needs nothing extra to make a name globally visible.

typescripttypescript
// globals.d.ts  (script -- no import/export lines)
declare const APP_NAME: string;
// APP_NAME is now available everywhere without importing.

A module-style declaration file behaves like any other module: the values it declares stay local until they are exported and imported by name.

typescripttypescript
// types.d.ts  (module -- contains export)
export declare function parseInput(raw: string): Record<string, string>;
 
// parseInput must be imported to use it.

For module packages, prefer export declare rather than a bare declare without export. For more on module organization, see declaration files in TypeScript.

When to Use Ambient Declarations in .ts Files

Ambient declarations are not only for declaration files. Use them in regular TypeScript source files when:

  • Your build tool injects a global, like a version string or an API base URL.
  • You are referencing a script loaded by a &lt;script&gt; tag in HTML.
  • You are adding types for a module that lacks them, using a declare module block. See type untyped packages in TypeScript for a full walkthrough.

The example below matches the first case: a bundler like webpack replaces build-time constants with literal values, so the running code never actually calls a function to get them.

typescripttypescript
// app.ts
// Injected by webpack DefinePlugin
declare const __VERSION__: string;
declare const __BUILD_TIME__: number;
 
console.log(`App v${__VERSION__}, built at ${new Date(__BUILD_TIME__).toISOString()}`);

Common Mistake: Implementing a declare

A declare declaration must not include an implementation, because ambient contexts only describe a shape and never generate code. Writing a function body alongside it triggers a compiler error, as shown below.

typescripttypescript
declare function greet(name: string): string {
  //                                         ^
  // An implementation cannot be declared in ambient contexts.
  return `Hello, ${name}!`;
}

The fix depends on what you are actually writing. Remove the body if this is meant to be a pure ambient declaration, or remove declare entirely and keep the body if this is meant to be a real, runnable function.

typescripttypescript
// Correct: declare without body
declare function greet(name: string): string;
 
// Also correct: regular function with body
function greet(name: string): string {
  return `Hello, ${name}!`;
}

The two forms are mutually exclusive. Use declare when the implementation is elsewhere. Use a regular function when you are providing the implementation.

Rune AI

Rune AI

Key Insights

  • declare tells TypeScript a value exists at runtime without providing an implementation.
  • declare var, declare function, declare class, and declare namespace cover the most common patterns.
  • Ambient declarations produce no JavaScript output and exist only at compile time.
  • You can use declare in .d.ts files or inside .ts files for global augmentation.
  • If a type can be inferred or imported normally, you do not need declare.
RunePowered by Rune AI

Frequently Asked Questions

What does ambient mean in TypeScript?

Ambient means that the declaration describes something that exists outside the TypeScript compilation. It tells the compiler about a variable, function, or class that is already present at runtime from JavaScript code, a script tag, or a browser API.

Can I use declare inside a normal .ts file?

Yes. The declare keyword works in both .ts and .d.ts files. In .ts files it is useful for declaring global variables or module types when the implementation lives elsewhere.

What is the difference between declare and export?

declare tells TypeScript that a value exists at runtime. export makes the declaration available to other modules. They answer different questions: declare answers "this exists" and export answers "this is shared."

Conclusion

Ambient declarations are TypeScript's way of describing the outside world. They tell the compiler about variables, functions, classes, and namespaces that exist at runtime but come from JavaScript, the browser, or a third-party script. Use them whenever you need types for code you did not write in TypeScript.