Recipe · Modules and packages

How do I declare a global variable or add a property to window?

  • Browser (no bundler)
  • declare global
  • Window
  • .d.ts
  • Checked with TypeScript 5.9

The problem My page script uses window.dataLayer from an analytics snippet and an __APP_VERSION__ constant my bundler injects. tsc says neither exists. Where do I declare them?

The short answer

Create a globals.d.ts, make it a module with export {}, and declare the globals inside declare global { … }. Include the file in jsconfig.json:

globals.d.ts TypeScript 5.9.3 TypeScript 5.9.3, run 21 October 2026: No errors. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. No errors. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. No errors. checked 21 October 2026
export {};

declare global {
  interface Window {
    dataLayer: unknown[];
  }

  /** Replaced with the release number by the build. */
  const __APP_VERSION__: string;
}
Globals declared in a .d.ts file, used from a page script.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "lib": ["es2022", "dom", "dom.iterable"],
    "types": []
  },
  "include": ["*.js", "*.d.ts"]
}

The page script then uses both without a cast:

analytics.js TypeScript 5.9.3 TypeScript 5.9.3, run 21 October 2026: No errors. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. No errors. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. No errors. checked 21 October 2026
/**
 * @param {string} event
 * @param {Record<string, string | number>} [details]
 */
export function track(event, details = {}) {
  window.dataLayer.push({ event, version: __APP_VERSION__, ...details });
}

track("checkout", { total: 4999 });
Globals declared in a .d.ts file, used from a page script.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "lib": ["es2022", "dom", "dom.iterable"],
    "types": []
  },
  "include": ["*.js", "*.d.ts"]
}

Why it works

interface Window { dataLayer: unknown[] } does not replace the DOM’s Window type; interfaces with the same name merge, so the existing interface gains a property. const __APP_VERSION__: string declares a global name with no value: a promise that something (here the bundler’s define step) provides it at runtime. Because the file has export {}, it is a module, and declare global is how a module adds to the global scope.

The same thing in TypeScript

A TypeScript project writes the same globals.d.ts; declaration files are the same in both kinds of project, so this example has no TypeScript tab.

What goes wrong

Without the declarations, window.dataLayer is TS2339 and __APP_VERSION__ is an unknown name (TS2304):

analytics.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: analytics.js(4,10): error TS2339: Property 'dataLayer' does not exist on type 'Window & typeof globalThis'. analytics.js(4,43): error TS2304: Cannot find name '__APP_VERSION__'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. analytics.js(4,10): error TS2339: Property 'dataLayer' does not exist on type 'Window & typeof globalThis'. analytics.js(4,43): error TS2304: Cannot find name '__APP_VERSION__'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. analytics.js(4,10): error TS2339: Property 'dataLayer' does not exist on type 'Window & typeof globalThis'. analytics.js(4,43): error TS2304: Cannot find name '__APP_VERSION__'. checked 21 October 2026
/** @param {string} event */
export function track(event) {
  window.dataLayer.push({ event, version: __APP_VERSION__ });error TS2339: Property 'dataLayer' does not exist on type 'Window & typeof globalThis'.error TS2304: Cannot find name '__APP_VERSION__'.
}
The same script without the declarations.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "lib": ["es2022", "dom", "dom.iterable"],
    "types": []
  },
  "include": ["*.js"]
}

Gotchas

  • The .d.ts file must be in the program. List it in include (or include a folder it sits in); a declaration file nobody includes changes nothing.
  • globalThis.x and window.x differ. A declare const is not a property of globalThis, so globalThis.__APP_VERSION__ is TS2339; reading an undeclared globalThis.x is TS7017.
  • Declarations are not checks. If the analytics snippet fails to load, window.dataLayer is undefined at runtime whatever the type says; declare it optional (dataLayer?: unknown[]) if your code must handle that.

Questions

Can I declare a global in a .js file instead of a .d.ts?

Not with declare, which is TypeScript syntax. A .js file can create a global by assigning globalThis.x = … in a checked file, but a .d.ts is the place to describe globals that something else creates.

Why declare var in some examples and const here?

Inside declare global, const makes the name read-only to the checker. declare var also adds it as a property of globalThis and window; use it when code reads window.__APP_VERSION__ as well.

Sources

Last reviewed 21 October 2026.