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
- Use the declare keyword to type existing runtime code
- Understand .d.ts declaration files (types, no runtime)
- Declare a global variable or function for a script-tag library
- Declare a module for an untyped JS package
- Install community types from @types / DefinitelyTyped
- Pull in declarations with triple-slash reference directives
💡 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.03. @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
// 6Common Errors (and the fix)
- ❌ "Could not find a declaration file for module 'foo'" — the package ships no types. ✅ npm i -D @types/foo, or add declare module "foo" { ... } in a .d.ts.
- ❌ Runtime crash even though it type-checked — you declared a global that isn't actually loaded. ✅ declare emits nothing; make sure the script/value really exists at runtime.
- ❌ "Cannot find name 'process'" — Node's ambient types aren't included. ✅ npm i -D @types/node (and add "node" to compilerOptions.types if needed).
- ❌ Putting runtime code in a .d.ts — declaration files are type-only. ✅ Use only declare/type/interface; move real code to a .ts file.
Pro Tips
- 💡 Keep hand-written declarations in a types/ folder and make sure tsconfig's include covers it.
- 💡 Describe only what you use. A partial but accurate declare module beats a sprawling, wrong one.
- 💡 Prefer a library's bundled types over @types when both exist — bundled ones can't drift out of sync.
- 💡 Wildcard modules (declare module "*.css") are the standard way to import non-JS assets in a typed build.
📚 Keep Learning
- 📖 TypeScript Handbook — Declaration Files
- 📖 Declaration Files — By Example
- 📖 Consuming declaration files (@types)
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
- ✅ declare types existing runtime code and emits no JavaScript
- ✅ .d.ts files hold types only — describing JS you didn't write
- ✅ declare global types script-tag globals; declare module "x" types untyped packages
- ✅ @types/... packages from DefinitelyTyped ship community declarations
- ✅ Triple-slash directives pull in other declaration files or lib types
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
- Previous: Namespaces
- Next: Typing Promises & async/await — Promise<T>, Awaited<T>, and typing a fetch wrapper
- Quick reference: TypeScript cheat sheet
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.