CodeFixesHub
    programming tutorial

    Declaration Files for Global Variables and Functions

    Write safe TypeScript globals with declaration files—practical examples, debugging tips, and best practices. Read the full guide and apply today.

    article details

    Quick Overview

    TypeScript
    Category
    Sep 24
    Published
    21
    Min Read
    2K
    Words
    article summary

    Write safe TypeScript globals with declaration files—practical examples, debugging tips, and best practices. Read the full guide and apply today.

    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.ts files 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.json configured. Recommended options include strict: true, and ensure typeRoots or types are set if you want to limit visible global types
    • Basic familiarity with interfaces, union types, and ambient declare syntax

    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:

    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 GlobalAnalytics

    This 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:

    ts
    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:

    ts
    declare interface AppConfig {
      [key: string]: string | number | boolean
    }
    
    declare var __APP_CONFIG__: AppConfig

    When 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:

    ts
    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:

    ts
    type ServerKeys = 'user_id' | 'last_login'
    
    type CamelCaseKeys = {
      [K in ServerKeys as Camelize<K>]: string
    }
    
    // declare global var later: declare var serverData: CamelCaseKeys

    Here 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:

    ts
    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: Excluding null and undefined.

    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:

    ts
    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:

    ts
    // 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:

    ts
    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:

    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.ts to express multiple runtime configurations.
    • Compose utility types to keep declarations DRY. Use Pick, Omit, Extract, and Exclude to 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 + interface merging 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 namespace when 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 any for global declarations.
    • Keep global declaration files small and well-documented.
    • Use declare global {} in a module file (one with at least one export or import) 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 any global variables like declare 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 typeRoots misconfiguration that can cause duplicate identifier errors. Keep typeRoots narrow 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, check tsconfig.json include, typeRoots, and types settings.
    • Use tsc --traceResolution to 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.ts so the rest of the app gets accurate typings for analytics.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.ts files including export as namespace so 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 resource for patterns.

    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.

    article completed

    Great Work!

    You've successfully completed this TypeScript tutorial. Ready to explore more concepts and enhance your development skills?

    share this article

    Found This Helpful?

    Share this TypeScript tutorial with your network and help other developers learn!

    continue learning

    Related Articles

    Discover more programming tutorials and solutions related to this topic.

    No related articles found.

    Try browsing our categories for more content.

    Content Sync Status
    Offline
    Changes: 0
    Last sync: 11:19:54 PM
    Next sync: 60s
    Loading CodeFixesHub...