Declaration Files for Global Variables and Functions
Introduction
Global variables and globally available functions are common in JavaScript ecosystems: polyfills, third-party libraries loaded via script tags, and environment-injected values (like process, window, or host-provided APIs) all create or rely on globals. In TypeScript projects, these globals present a challenge: without explicit type declarations, developers lose autocompletion, type checking, and compiler guarantees. Declaration files (.d.ts) provide the contract between untyped global behavior and the TypeScript type system.
This tutorial dives deep into authoring, structuring, and maintaining declaration files for global variables and functions. You'll learn how to: design global declaration files that are safe and maintainable; scope declarations to avoid name clashes; create ambient modules and global augmentations; write typed overloads for global functions; combine type utilities safely; and debug common issues when TypeScript still complains despite existing declarations.
Intended for intermediate developers, the guide assumes familiarity with basic TypeScript types and compiler options. Through practical examples and step-by-step guidance, you'll finish the article able to author robust .d.ts files, augment third-party types, and adopt best practices that avoid brittle global typings in long-lived codebases.
What you will learn:
- When to use ambient declarations vs. module augmentation
- How to author and distribute
.d.tsfiles for libraries that create globals - Best patterns for typing global objects with index signatures and mapped types
- Techniques to avoid global namespace pollution and to combine declaration merging safely
By the end you'll be equipped to add and maintain clear, compiler-friendly declarations for globals in both libraries and applications.
Background & Context
TypeScript treats files with type declarations differently. Declaration files (.d.ts) describe the shape of code without emitting JavaScript. For globals, TypeScript supports "ambient" declarations using the declare global {} block or top-level declare var/declare function statements. Ambient declarations can be included in a library's distributed .d.ts or placed in your project under an @types-like location.
Correctly authored declaration files provide editor tooling (IntelliSense), prevent incorrect uses of runtime APIs, and document runtime expectations. Poorly scoped or overly broad global declarations cause type collisions and subtle bugs — for example, declaring declare var fetch: any removes useful type-safety. Instead, accurate declarations narrow usages and keep the compiler helpful.
Some features such as dynamically keyed objects benefit from index signatures; mapped types help when you want to create derived shapes programmatically. For deeper transformations of types you may refer to guides on Index signatures and the Introduction to Mapped Types.
Key Takeaways
- Declaration files (.d.ts) let you tell TypeScript about runtime globals without emitting code.
- Prefer narrow, explicit declarations to
any— use typed overloads, interfaces, and mapped types where appropriate. - Use
declare global {}for app-level augmentation and module augmentation for library patches. - Guard declaration merging and name collisions with namespaces and unique names.
- Use utility types (Pick, Omit, NonNullable, Extract/Exclude) to safely transform global types.
Prerequisites & Setup
Before proceeding, ensure you have:
- Node.js + npm installed
- TypeScript installed (local dev dependency recommended):
npm install --save-dev typescript - A project with
tsconfig.jsonconfigured. Recommended options includestrict: true, and ensuretypeRootsortypesare set if you want to limit visible global types - Basic familiarity with interfaces, union types, and ambient
declaresyntax
Create a sample project and add a src directory. We will create .d.ts files in a types/ folder, and add that folder to tsconfig.json via typeRoots or include the .d.ts files directly using include.
Main Tutorial Sections
1) Anatomy of a Simple Global Declaration (100-150 words)
Start with the smallest useful declaration. Suppose a script provides a global analytics object with method track(eventName: string, payload?: object). Create types/globals.d.ts:
// types/globals.d.ts
declare interface AnalyticsPayload {
[key: string]: unknown
}
declare namespace GlobalAnalytics {
function track(eventName: string, payload?: AnalyticsPayload): void
}
declare var analytics: typeof GlobalAnalyticsThis declares an interface for payloads, a namespace containing the function, and a var typed to the namespace shape. Place this file in a directory included by TypeScript (typeRoots or include). This approach narrows types and provides editor feedback before runtime.
2) Using declare global vs Top-Level declare (100-150 words)
For project-scoped augmentations, use declare global {} inside a module to merge types into the global scope without leaking top-level names:
export {}
declare global {
interface Window {
__MY_APP__: { version: string }
}
}This is safe in a file that has at least one top-level import/export so it is treated as a module. The export {} pattern prevents its declarations from being emitted as global script-level declarations in build steps. Prefer declare global in application code; use top-level declare var or declare function when authoring library .d.ts files intended for global consumption.
3) Typing Global Objects with Index Signatures (100-150 words)
Global objects sometimes hold dynamic keys. Index signatures let you type these patterns, but avoid any. Example: a global configuration object with string keys and values of different allowed types:
declare interface AppConfig {
[key: string]: string | number | boolean
}
declare var __APP_CONFIG__: AppConfigWhen dynamic keys are required, use index signatures rather than broad Record<string, any>. If you need more advanced patterns, review our article on Index signatures for more patterns and pitfalls.
4) Combining Mapped Types with Global Declarations (100-150 words)
Mapped types help generate related global shapes. Suppose your runtime creates a set of feature flags keyed by a union type. Use a mapped type to keep the global declaration consistent:
type Feature = 'auth' | 'payments' | 'analytics'
declare global {
type FeatureFlags = { [K in Feature]: boolean }
var __FEATURE_FLAGS__: FeatureFlags
}Mapped types are powerful when creating derived global shapes. For background on mapped transformations and syntax, see Basic Mapped Type Syntax and the Introduction to Mapped Types.
5) Key Remapping and Global Key Transformations (100-150 words)
Key remapping using as in mapped types is useful when global APIs provide transformed key sets, such as converting snake_case server keys to camelCase client keys at runtime. You can mirror that transformation in a declaration:
type ServerKeys = 'user_id' | 'last_login'
type CamelCaseKeys = {
[K in ServerKeys as Camelize<K>]: string
}
// declare global var later: declare var serverData: CamelCaseKeysHere Camelize<K> would be a type-level helper. For reference and advanced examples of key remapping, see Key Remapping with as in Mapped Types — A Practical Guide.
6) Dealing with Optional and Nullable Globals (100-150 words)
Some globals might be absent in certain environments. Avoid marking everything optional; instead combine utilities like NonNullable<T> to clearly express runtime expectations. Example:
declare var maybeFeature: boolean | null | undefined
// Use NonNullable in your code to assert runtime presence safely
function useFeature() {
const flag: NonNullable<typeof maybeFeature> = maybeFeature as NonNullable<typeof maybeFeature>
// guard or fallback before using
}Using NonNullable<T> helps transform declarations into stricter types in your codebase. For patterns and pitfalls, consult Using NonNullable
7) Narrowing and Runtime Checks for Globals (100-150 words)
Even with declarations, runtime values may vary. Use control flow analysis and runtime guards before calling global APIs. For example:
declare var external: unknown
if (typeof external === 'object' && external !== null && 'doThing' in external) {
const x = external as { doThing: (s: string) => void }
x.doThing('ok')
}Use TypeScript narrowing patterns like typeof and in. For a deep dive on narrowing techniques and how the compiler performs control flow analysis, see Control Flow Analysis for Type Narrowing in TypeScript.
8) Augmenting Global Types from Third-Party Libraries (100-150 words)
When a third-party library augments the global scope or is loaded via a script tag, create an augmentation file that merges with the existing global declarations. Example for augmenting Window:
// types/global-augment.d.ts
import 'some-module' // keep as module
declare global {
interface Window {
thirdPartyAPI?: { init: () => void }
}
}Place this file in your typeRoots or ensure it's included. If the library provides its own types, prefer module augmentation using declare module to avoid duplicate global names.
9) Using Utility Types (Pick/Omit/Extract/Exclude) with Globals (100-150 words)
Utility types let you derive safer shapes instead of repeating definitions. For example, pick a subset of a global object's properties for a smaller API surface:
declare interface GlobalStore {
sessionId: string
userId: string
debug: boolean
}
declare var store: GlobalStore
type PublicStore = Pick<GlobalStore, 'sessionId' | 'userId'>When you need to extract or exclude union members, Extract<T, U> and Exclude<T, U> are useful to keep declarations accurate. See our deep dives on Using Extract<T, U> and Using Exclude<T, U> for detailed patterns.
10) Distributing Declaration Files for Libraries that Create Globals (100-150 words)
If you publish a library that installs a global (e.g., via a script tag), include a top-level .d.ts pointing to your global declarations and set the types field in package.json:
{
"name": "my-lib",
"main": "dist/index.js",
"types": "dist/index.d.ts"
}In dist/index.d.ts: export the ambient declarations or reference a globals.d.ts using /// <reference path="globals.d.ts" />. Be conservative: avoid populating too many global names and document installation steps. Consumers should be able to opt-in to your global types using typeRoots or by including your package's types automatically when installed.
Advanced Techniques
When authoring robust declaration files for globals, consider these expert approaches:
- Use conditional types for environment-specific shapes. For instance, a type that resolves differently depending on whether you target browser or Node environments can be modeled with conditional types. This allows a single
.d.tsto express multiple runtime configurations. - Compose utility types to keep declarations DRY. Use
Pick,Omit,Extract, andExcludeto derive new shapes from a canonical interface rather than re-defining fields. See Using Pick<T, K> and Using Omit<T, K> for examples. - Create small helper interfaces and re-export them for both runtime code and other declaration files to avoid duplication.
- Leverage
namespace+interfacemerging for extensible global APIs. For example, a library can declare a namespace with types and a global var typed to that namespace. This pattern supports incremental extension by downstream consumers. - Use
declare module+export as namespacewhen targeting UMD-style libraries to support both module and global consumers.
These techniques help maintain consistency across large codebases and avoid brittle, one-off declarations.
Best Practices & Common Pitfalls
Dos:
- Prefer explicit, narrow types over
anyfor global declarations. - Keep global declaration files small and well-documented.
- Use
declare global {}in a module file (one with at least oneexportorimport) to avoid accidental global leakage. - Validate declarations against runtime behavior—unit tests that execute the library in an environment help catch mismatches.
Don'ts / Common pitfalls:
- Don’t silently declare wide
anyglobal variables likedeclare var foo: any— this disables helpful checks. - Avoid naming collisions. If multiple libs declare the same global, prefer module augmentation or unique names to avoid merging surprises.
- Beware of triple-slash references and
typeRootsmisconfiguration that can cause duplicate identifier errors. KeeptypeRootsnarrow if you control the app's ambient environment. - Don’t assume a runtime global exists; always guard or provide fallbacks in code that consumes them. Use narrowing patterns described earlier and consult guides on Type Narrowing and related narrowing techniques like typeof checks and in operator narrowing.
Troubleshooting tips:
- If TypeScript ignores your
.d.ts, checktsconfig.jsoninclude,typeRoots, andtypessettings. - Use
tsc --traceResolutionto see how the compiler resolves types and find duplicate/hidden definitions. - Run the type checker in strict mode to catch subtle mismatches earlier.
Real-World Applications
Declaration files for globals are useful in multiple real scenarios:
- Legacy integration: When integrating a legacy analytics script, write concise
.d.tsso the rest of the app gets accurate typings foranalytics.track. - Embedding host APIs: Electron, webviews, or game engines often inject host-provided globals; typing them provides safer runtime access.
- Polyfills and shims: When you load a polyfill in older browsers, add ambient declarations so modern typings are available in your code even if the runtime is supplemented by a script.
- UMD libraries: Libraries offering both modules and globals should publish
.d.tsfiles includingexport as namespaceso both consumers can benefit from types.
In each case, keep declarations minimal and well-documented, and consider the consumers' tooling — for instance, documenting required typeRoots changes or providing a separate @types package if global changes are significant.
Conclusion & Next Steps
Declaration files for globals are a small but critical part of TypeScript's typing story. With careful design, you can provide precise types that improve developer experience and reduce runtime mistakes. Start by writing small, narrow .d.ts files, verify them with runtime checks, and adopt utility types and mapped types to keep them DRY and accurate.
Next steps: practice by typing a few real-world globals in your project, add tests that validate runtime assumptions, and explore related topics such as mapped types and utility type transformations.
For deeper reading on mapped types, index signatures, and narrowing, see the linked articles throughout this guide.
Enhanced FAQ
Q1: Where should I place .d.ts files for project-specific globals?
A1: Put them under a types/ or @types/ folder and ensure tsconfig.json includes that location (either via include or typeRoots). If you prefer implicit discovery, place them in a folder already covered by typeRoots. Use /// <reference path> only when necessary; prefer typeRoots and include for clarity.
Q2: Should I use declare var or declare global {}?
A2: Use declare var for simple top-level ambient declarations, especially in library .d.ts files meant for global consumption. Use declare global {} inside a module file when you want to augment the global scope without creating top-level script declarations — this is safer in application code and avoids leaking names across builds.
Q3: How do I avoid name collisions when multiple packages declare the same global?
A3: Favor module-based approaches and unique prefixes. If unavoidable, document merging rules and ensure compatibility by aligning declarations. When publishing a library, consider exposing a module API instead of a global to avoid collisions. If a global is required, namespace it under a unique object (e.g., window.__myLib__) rather than adding top-level names.
Q4: Can I use mapped types and key remapping in .d.ts files?
A4: Yes — mapped types, conditional types, and key remapping are supported in declaration files. These features are great for deriving shapes from unions and for keeping a single source of truth for global shapes. See mappings examples and the Key Remapping with as guide for advanced patterns.
Q5: What if TypeScript still can't find my declarations?
A5: Run tsc --traceResolution to inspect how the compiler resolves types. Check tsconfig.json typeRoots, types, and paths. Also ensure the file is not excluded by exclude or missing from include. If you publish to npm, ensure the types field points to the correct .d.ts path.
Q6: How do I write declarations for globals that sometimes don't exist at runtime?
A6: Type those globals as potentially undefined or nullable (e.g., string | undefined) and always guard access with runtime checks. Use NonNullable<T> when you need to derive a stricter type after a guard. Refer to the section above and the Using NonNullable
Q7: When should I prefer module augmentation over global augmentation?
A7: Prefer module augmentation when you want to add types to an existing module (e.g., add fields to express.Request). Use global augmentation when the runtime truly installs values on the global object. Module augmentation is more modular and less likely to cause name collisions.
Q8: Can declaration files include implementation details?
A8: No — .d.ts files only contain type declarations. They should not include runtime code. If you need initialization behavior, include documentation and runtime code in the distributed package separately; the .d.ts should accurately describe the runtime API.
Q9: How do utility types like Extract and Exclude fit into global declarations?
A9: Use them to pick or filter union members when modeling global APIs spread across versions or environments. For example, exclude deprecated event names from an event union with Exclude<T, 'deprecatedEvent'>. See Using Extract<T, U> and Using Exclude<T, U> for detailed guidance.
Q10: Are there performance implications of large declaration files?
A10: Very large or complex .d.ts files with heavy conditional and recursive types can affect TypeScript compile and editor responsiveness. Split declarations sensibly, prefer simpler types for widely-shared globals, and move complex type-level computation to opt-in modules when possible. Use types configuration to limit the set of types loaded in large monorepos.
