Ambient Declarations & @types

Reviewed & published by Brayan K

An ambient declaration uses the declare keyword to describe the type of code that already exists at runtime — a global, a module, or an untyped library — without generating any JavaScript of its own.

Part of the free TypeScript course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

An ambient declaration is like a label on a sealed box. The box (a JavaScript global or untyped package) is already on the shelf; the label tells everyone what's inside without you having to open or build it. declare writes the label. @types packages are pre-printed labels the community made for the most common boxes, so you don't have to write them yourself.

1. The declare Keyword

The declare keyword introduces an ambient declaration: it states a value or function exists and what its type is, but produces no JavaScript. Use it to type a global a third-party script attaches to window, an analytics function, or anything your TypeScript references but doesn't itself create.

// Ambient declarations describe things that exist WITHOUT defining them.
// You write them in a .d.ts file (or any file) using 'declare':

declare const analytics: {
  track(event: string): void;
};

declare function gtag(command: string, ...args: unknown[]): void;

declare global {
  interface Window {
    myWidget: { open(): void };
  }
}

// Now this type-checks even though NONE of it is defined in your TS:
analytics.track("signup");   // ✅ typed
window.myWidget.open();      // ✅ typed
// There is NO emitted JS for any of the lines above.
// The 'declare' keyword adds NO runtime code — it only TELLS the compiler that
// something already exists, so type-checking passes. To SEE what it describes,
// here is the actual runtime value an ambient declaration is talking about.

// Suppose an untyped script tag loaded a global library: window.analytics = {...}
// In a .d.ts you'd write:
//   declare const analytics: { track(event: string): void };
// At RUNTIME it's simply a real object that already exists somewhere:
const analytics = {
  track(event) { return "tracked: " + event; },
};

console.log(analytics.track("signup"));   // tracked: signup
console.log(analytics.track("purchase")); // tracked: purchase

// 'declare' is a PROMISE to the compiler: "trust me, this exists at runtime."
// It emits nothing, so if the promise is wrong you get a runtime crash.

2. Declaring a Module for an Untyped Library

When you import a package that ships no types, TypeScript errors with "Could not find a declaration file". Fix it with declare module "name" { ... } in a .d.ts, describing just the parts you use. The same trick — with a wildcard like declare module "*.svg" — types non-code imports such as images.

// Give a name and a shape to a runtime-only package in a .d.ts file:
declare module "tiny-math" {
  export function add(a: number, b: number): number;
  export const VERSION: string;
}

// A quick-and-dirty wildcard for assets/untyped modules:
declare module "*.svg" {
  const src: string;
  export default src;
}

// Now your imports are typed:
import { add, VERSION } from "tiny-math"; // ✅
import logo from "./logo.svg";            // ✅ logo: string
// Declaring a MODULE for an untyped JS library. In a .d.ts you'd write:
//   declare module "tiny-math" {
//     export function add(a: number, b: number): number;
//     export const VERSION: string;
//   }
// That gives types for the import WITHOUT a single line of runtime code.
// Here is the actual JS module such a declaration describes:

const tinyMath = {
  VERSION: "1.0.0",
  add(a, b) { return a + b; },
};

// The consumer code that 'declare module' makes type-safe:
console.log(tinyMath.add(2, 3));  // 5
console.log(tinyMath.VERSION);    // 1.0.0

3. @types, DefinitelyTyped & Triple-Slash

For popular libraries you almost never write declarations by hand — the community publishes them on npm under the @types scope (from the DefinitelyTyped repo). Install the runtime package and its types separately: npm i lodash then npm i -D @types/lodash. A triple-slash directive like /// <reference types="node" /> pulls another declaration set in.

// 1) Install types for an untyped package (runtime + types are separate):
//    npm install lodash
//    npm install --save-dev @types/lodash
import _ from "lodash";
_.chunk([1, 2, 3, 4], 2); // ✅ fully typed by @types/lodash

// 2) Triple-slash references pull in another declaration file or lib:
/// <reference types="node" />        // include @types/node ambient types
/// <reference path="./globals.d.ts" /> // include a local declaration file

// 3) Node's process is itself an ambient declaration from @types/node:
process.env.NODE_ENV; // ✅ typed as string | undefined
// You rarely write these by hand — DefinitelyTyped publishes them for you on npm
// under the @types scope. The runtime package stays the SAME; @types just adds
// the .d.ts files. Here is the runtime a typical @types package describes:

// e.g. after:  npm i lodash  &&  npm i -D @types/lodash
const _ = {
  chunk(arr, size) {
    const out = [];
    for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size));
    return out;
  },
};

console.log(_.chunk([1, 2, 3, 4, 5], 2)); // [ [1,2], [3,4], [5] ]

// The @types/lodash package adds:  declare module "lodash" { export function chunk... }
// so 'import _ from "lodash"' is fully typed — no runtime change at all.

🎯 Your Turn

Given the ambient declaration declare const config: { apiUrl: string; retries: number }, build the runtime object it describes. Fill in the blank marked ___, then run it.

// 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".
// In a .d.ts you'd write:  declare const config: { apiUrl: string; retries: number };
// Build the RUNTIME object that ambient declaration is describing.

const config = {
  apiUrl: "https://api.example.com",
  retries: ___,                 // 👉 the number 3
};

console.log(config.apiUrl);      // https://api.example.com
console.log(config.retries * 2); // 👉 retries(3) * 2 -> 6

// ✅ Expected output:
//    https://api.example.com
//    6

Common Errors (and the fix)

Pro Tips

📚 Keep Learning

Mini-Challenge: The Runtime Behind a Global Declaration

No blanks this time — just a brief and a starting outline. Build the object the ambient declaration describes, run it, and check your output against the example in the comments.

// 🎯 MINI-CHALLENGE: model the runtime a global 'logger' declaration describes.
// In a .d.ts you'd have:
//   declare const logger: { level: string; log(msg: string): string };
// Build that object at runtime:
// 1. logger.level starts as "info".
// 2. logger.log(msg) returns "[" + logger.level + "] " + msg.
// 3. Log logger.log("started") and logger.level.
// 4. Set logger.level = "debug", then log logger.log("verbose").
//
// ✅ Expected output:
//    [info] started
//    info
//    [debug] verbose

// your code here

🎉 Lesson Complete

Practice quiz

What does the declare keyword do?

  • Generates JavaScript that creates the value
  • Imports a module at runtime
  • Tells the compiler something exists, emitting no JavaScript
  • Validates a value before use

Answer: Tells the compiler something exists, emitting no JavaScript. declare introduces an ambient declaration: it states a thing exists and its type, but produces no JavaScript output.

What does a .d.ts declaration file contain?

  • Only type information - no runtime code
  • Runtime code and types together
  • Compiled JavaScript
  • Just import statements

Answer: Only type information - no runtime code. A .d.ts file holds type information only; it describes the shape of existing JavaScript with no runtime code.

What is the risk of a declare that promises a global which isn't actually loaded?

  • A compile error
  • The global is created automatically
  • Nothing - declare guarantees existence
  • Type-checking passes but the program crashes at runtime

Answer: Type-checking passes but the program crashes at runtime. declare is a one-way promise that emits nothing. If the value isn't really there, checking passes but the code crashes at runtime.

How do you add types for an imported package that ships no types?

  • Rewrite the package in TypeScript
  • Use declare module "name" { export ... } in a .d.ts
  • Add a // @ts-ignore comment
  • Set strict to false

Answer: Use declare module "name" { export ... } in a .d.ts. declare module "name" { ... } in a .d.ts describes the parts of an untyped package you use, fixing the missing-declaration error.

What does a bare declare module "name"; with no body do?

  • Types the whole module as any - silencing the error with no safety
  • Errors at compile time
  • Generates stub functions
  • Imports the module's real types

Answer: Types the whole module as any - silencing the error with no safety. A bodyless declare module types the module as any: it silences the error but gives you zero type safety.

What is DefinitelyTyped?

  • A TypeScript compiler flag
  • A bundler for TypeScript
  • A community repository of declaration files published under @types on npm
  • A linting tool

Answer: A community repository of declaration files published under @types on npm. DefinitelyTyped is a community repo of .d.ts files for popular untyped libraries, published to npm under the @types scope.

After installing @types/lodash, what changes at runtime?

  • lodash runs faster
  • Nothing - @types adds only type information
  • lodash is recompiled
  • New functions are added to lodash

Answer: Nothing - @types adds only type information. @types packages add only .d.ts files; the runtime package is unchanged. Types are erased at compile time.

What does a triple-slash directive like /// <reference types="node" /> do?

  • Runs Node at compile time
  • Marks a file as a comment
  • Disables type checking for the file
  • Pulls in another declaration set or lib's ambient types

Answer: Pulls in another declaration set or lib's ambient types. A triple-slash reference directive includes another declaration file or library's ambient types into the current compilation.

Which wildcard module declaration is the standard way to type non-JS asset imports?

  • declare global { ... }
  • declare module "*.svg" { ... }
  • declare const assets: any
  • import type { Svg }

Answer: declare module "*.svg" { ... }. A wildcard like declare module "*.svg" types asset imports such as images in a typed build.

What should you NOT put in a .d.ts file?

  • Type aliases
  • interface definitions
  • Real runtime code
  • declare statements

Answer: Real runtime code. Declaration files are type-only; runtime code belongs in a .ts file. Use only declare, type, and interface in a .d.ts.

Continue this course

Frequently asked questions

What does the 'declare' keyword actually do?

It introduces an ambient declaration: it tells the TypeScript compiler that a value, function, class, or module exists and what its type is, WITHOUT generating any JavaScript. It's a promise that the thing is provided at runtime by something else (a script tag, a global, a JS-only package). If that promise is false, you get a runtime error because 'declare' emits nothing.

What is a .d.ts file?

A declaration file (.d.ts) contains only type information — no runtime code. It describes the shape of existing JavaScript so TypeScript can type-check code that uses it. Libraries ship .d.ts files (or you get them from @types) so you get autocomplete and checking for code you didn't write in TypeScript.

What are @types and DefinitelyTyped?

DefinitelyTyped is a huge community repository of declaration files for popular JavaScript libraries that don't ship their own types. Those files are published to npm under the @types scope — e.g. 'npm i -D @types/lodash' adds types for the 'lodash' runtime package. The runtime package and its @types are separate installs.

How do I add types for a package that has none?

Create a .d.ts file with 'declare module "package-name" { export ... }' describing the API you use, then make sure your tsconfig includes it. As a last resort 'declare module "package-name";' with no body types the module as 'any'. Triple-slash directives like '/// <reference types="node" />' can pull in other declaration files.

Related lessons