From e4a3758030f2baa3a49e38c51b9a82129d2201ac Mon Sep 17 00:00:00 2001 From: Togetic <31132515+Togetic@users.noreply.github.com> Date: Fri, 7 Aug 2026 16:09:50 +0200 Subject: [PATCH 1/2] fix(gtm): don't declare `dataLayer` on the global `Window` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The GTM registry augments the global `Window` with the whole `GoogleTagManagerApi`, which includes a required `dataLayer: DataLayer & { push: DataLayerPush }`. That makes this package irreconcilable with any other package declaring `Window.dataLayer`. The common case is `@gtm-support/core` (used by `@gtm-support/vue-gtm`), which declares it as optional: declare global { interface Window { dataLayer?: DataLayerObject[] } } The two cannot merge, so the merged `Window` fails its own `extends GoogleTagManagerApi` check and every consumer `Window` augmentation reports TS2430. It is not suppressable from consumer code. `tsc` 5.9.3 does not verify merged interfaces against their bases, so it stays silent. TypeScript 7 (tsgo) does check, and reports it — which this repo will hit itself once #827 lands. Declaring `dataLayer` globally is also inaccurate independently of the conflict: the dataLayer name is configurable via the `l` / `dataLayer` options, so `window.dataLayer` is not guaranteed to exist. The registry never relies on the global declaration for it either — it reads `(window as any)[dataLayerName]` and casts. `window.google_tag_manager` IS read directly, so that member stays declared. `GoogleTagManagerApi` itself is unchanged, so `useScriptGoogleTagManager()` and the `use()` return type keep their existing shape. Consumers should read the dataLayer through that typed proxy, which is also the only access path that respects a custom dataLayer name. --- .../script/src/runtime/registry/google-tag-manager.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/script/src/runtime/registry/google-tag-manager.ts b/packages/script/src/runtime/registry/google-tag-manager.ts index 98a6a967..81a16c84 100644 --- a/packages/script/src/runtime/registry/google-tag-manager.ts +++ b/packages/script/src/runtime/registry/google-tag-manager.ts @@ -72,9 +72,17 @@ export interface GoogleTagManagerApi { /** * Enhanced window type with GTM + * + * `dataLayer` is deliberately NOT declared globally. Its name is configurable through the + * `l` / `dataLayer` options, so `window.dataLayer` is not guaranteed to exist, and declaring it + * here makes this package irreconcilable with any other package that declares `Window.dataLayer` + * (for example `@gtm-support/core`, used by `@gtm-support/vue-gtm`): the two declarations cannot + * merge, so consumers of both get an unsuppressable TS2430 at every one of their own `Window` + * augmentations. Read it through the typed proxy returned by `useScriptGoogleTagManager()` + * instead, which is also the only access path that respects a custom dataLayer name. */ declare global { - interface Window extends GoogleTagManagerApi {} + interface Window extends Pick {} } export { GoogleTagManagerOptions } From db493ccf5479728d8910656821a214166a44d8e4 Mon Sep 17 00:00:00 2001 From: Harlan Wilton Date: Tue, 18 Aug 2026 14:21:46 +1000 Subject: [PATCH 2/2] test(gtm): guard `dataLayer` off the global `Window` Fails on the pre-fix declaration, passes on the fix. --- test/types/global-window.test-d.ts | 31 ++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 test/types/global-window.test-d.ts diff --git a/test/types/global-window.test-d.ts b/test/types/global-window.test-d.ts new file mode 100644 index 00000000..c940e36f --- /dev/null +++ b/test/types/global-window.test-d.ts @@ -0,0 +1,31 @@ +import type { GoogleTagManagerApi } from '../../packages/script/src/runtime/registry/google-tag-manager' +import { describe, expectTypeOf, it } from 'vitest' + +/** + * Regression guard for #852. + * + * `@gtm-support/core` (the engine behind `@gtm-support/vue-gtm`) declares + * `Window.dataLayer?: DataLayerObject[]`. Interface declarations merge, so if this package + * also declares `dataLayer` on the global `Window`, the merged `Window` fails this package's + * own `extends` clause. TypeScript then reports TS2430 at every one of the consumer's own + * `Window` augmentations, which the consumer cannot suppress. + * + * `dataLayer` must stay off the global `Window`. Its name is configurable through the + * `l` / `dataLayer` options anyway, so `window.dataLayer` was never guaranteed to exist. + * The proxy returned by `useScriptGoogleTagManager()` is the supported access path. + */ +describe('global `Window` augmentation', () => { + it('does not declare `dataLayer`', () => { + expectTypeOf<'dataLayer' extends keyof Window ? true : false>().toEqualTypeOf() + }) + + it('keeps `google_tag_manager` declared', () => { + // `useScriptGoogleTagManager`'s `use()` reads `window.google_tag_manager` directly. + expectTypeOf().toEqualTypeOf() + }) + + it('still types `dataLayer` on the GTM proxy API', () => { + expectTypeOf().not.toBeAny() + expectTypeOf().toBeCallableWith({ event: 'test' }) + }) +})