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:
export {};
declare global {
interface Window {
dataLayer: unknown[];
}
/** Replaced with the release number by the build. */
const __APP_VERSION__: string;
}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:
/**
* @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 });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):
/** @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__'.
}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.tsfile must be in the program. List it ininclude(or include a folder it sits in); a declaration file nobody includes changes nothing. globalThis.xandwindow.xdiffer. Adeclare constis not a property ofglobalThis, soglobalThis.__APP_VERSION__is TS2339; reading an undeclaredglobalThis.xis TS7017.- Declarations are not checks. If the analytics snippet fails to load,
window.dataLayerisundefinedat 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
- Declaration Merging (global augmentation) · TypeScript handbook
- Global .d.ts (declaration file templates) · TypeScript handbook
Last reviewed 21 October 2026.