Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion packages/script/src/runtime/registry/google-tag-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<GoogleTagManagerApi, 'google_tag_manager'> {}
}

export { GoogleTagManagerOptions }
Expand Down
31 changes: 31 additions & 0 deletions test/types/global-window.test-d.ts
Original file line number Diff line number Diff line change
@@ -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<false>()
})

it('keeps `google_tag_manager` declared', () => {
// `useScriptGoogleTagManager`'s `use()` reads `window.google_tag_manager` directly.
expectTypeOf<Window['google_tag_manager']>().toEqualTypeOf<GoogleTagManagerApi['google_tag_manager']>()
})

it('still types `dataLayer` on the GTM proxy API', () => {
expectTypeOf<GoogleTagManagerApi['dataLayer']>().not.toBeAny()
expectTypeOf<GoogleTagManagerApi['dataLayer']['push']>().toBeCallableWith({ event: 'test' })
})
})
Loading