Skip to content

fix(gtm): don't declare dataLayer on the global Window - #855

Open
Togetic wants to merge 1 commit into
nuxt:mainfrom
Togetic:fix/gtm-global-datalayer-conflict
Open

fix(gtm): don't declare dataLayer on the global Window#855
Togetic wants to merge 1 commit into
nuxt:mainfrom
Togetic:fix/gtm-global-datalayer-conflict

Conversation

@Togetic

@Togetic Togetic commented Aug 7, 2026

Copy link
Copy Markdown

Fixes #852.

Problem

The GTM registry augments the global Window with the entire GoogleTagManagerApi:

export interface GoogleTagManagerApi {
  google_tag_manager: GoogleTagManagerInstance
  dataLayer: DataLayer & { push: DataLayerPush }   // required
}

declare global {
  interface Window extends GoogleTagManagerApi {}
}

This makes @nuxt/scripts irreconcilable with any other package that declares Window.dataLayer. The common case is @gtm-support/core — the engine behind @gtm-support/vue-gtm, which a lot of Nuxt/Vue apps still use for GTM:

// @gtm-support/core/lib/index.d.ts
declare global {
  interface Window {
    dataLayer?: DataLayerObject[];   // optional, different element type
  }
}

The two declarations merge into one Window, which then fails its own extends GoogleTagManagerApi check. The result is TS2430 reported at every one of the consumer's own Window augmentations — nowhere near the actual cause, and not suppressable from consumer code, since neither declaration belongs to them.

tsc 5.9.3 never verifies merged interfaces against their bases, so it stays silent. TypeScript 7 (tsgo) does check, and reports it — so this repo will hit it itself once #827 lands.

The obvious fix does not work

#852 proposed making dataLayer optional. I checked, and that is not sufficient — there are two independent incompatibilities stacked, and making it optional only reveals the second:

Type 'DataLayerObject[]' is not assignable to type 'DataLayer & { push: DataLayerPush; }'.
  Type 'DataLayerObject[]' is not assignable to type '{ push: DataLayerPush; }'.
    Types of property 'push' are incompatible.
      Type '(...items: DataLayerObject[]) => number' is not assignable to type 'DataLayerPush'.
        Types of parameters 'items' and 'command' are incompatible.
          Type 'string' is not assignable to type 'DataLayerObject'.

Array<T>.push is (...items: T[]) => number, while DataLayerPush's first overload begins (command: string, ...). No amount of optionality reconciles that with a foreign element type. Any fix that keeps dataLayer in the global augmentation stays broken.

Repro

Four files, strict, skipLibCheck — one file per package plus a consumer that augments Window (as most apps do):

files
// gtm-support.d.ts  — verbatim shape from @gtm-support/core
interface DataLayerObject extends Record<string, any> { event?: string }
declare global { interface Window { dataLayer?: DataLayerObject[] } }
export { type DataLayerObject }
// consumer.ts — any first-party Window augmentation
export {}
declare global { interface Window { utmData?: Record<string, string> } }

plus nuxt-scripts.d.ts carrying the registry's type block.

Variant TS 7.0.2 tsc 5.9.3
current main TS2430 ✅ silent (never checks)
dataLayer made optional (#852's proposal) TS2430 ✅ silent
this PR ✅ clean ✅ clean

The fix

Declare only the member that is actually global and actually unambiguous:

-  interface Window extends GoogleTagManagerApi {}
+  interface Window extends Pick<GoogleTagManagerApi, 'google_tag_manager'> {}

Why this is safe

  • GoogleTagManagerApi is untouched, so useScriptGoogleTagManager<T extends GoogleTagManagerApi>() and the use() return type keep their exact shape. Anyone reading dataLayer off the returned proxy is unaffected.
  • The registry never relied on the global declaration for dataLayer. It reads (window as any)[dataLayerName] and casts — because the name is configurable. window.google_tag_manager is read directly (use()), which is why that member stays declared.
  • Nothing else in this repo depends on the global dataLayer typing. The four window.dataLayer occurrences are all inside innerHTML template strings (playground/.../unhead.vue, scripts/generate-sizes.ts), prose in an example (examples/regional-consent/app.vue), or already (window as any) (test/nuxt-runtime/consent-default.nuxt.test.ts).
  • Declaring it globally was inaccurate anyway. The dataLayer name is configurable via the l / dataLayer options, so window.dataLayer is not guaranteed to exist; the typed proxy is the only access path that respects a custom name.

Note on scope

This is a type-level change only — zero runtime bytes.

It is technically breaking for anyone who reads window.dataLayer directly and relies on this package to type it. That was arguably never a promise this package should have made (the name is configurable), and such code keeps working with a one-line local augmentation or by using the useScriptGoogleTagManager() proxy. Happy to put it behind a major, or to add the narrower Window { dataLayer?: unknown } shim instead, if you would prefer — whichever you would rather land.

I verified the above with the minimal reproduction (exact declarations from both packages, TS 7.0.2 and 5.9.3); I have not run the full repo test suite locally.

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 nuxt#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<T extends GoogleTagManagerApi>()` 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.
@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

@Togetic is attempting to deploy a commit to the Nuxt Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 54452633-bba8-4e30-a576-2da37a447b21

📥 Commits

Reviewing files that changed from the base of the PR and between d0324e7 and e4a3758.

📒 Files selected for processing (1)
  • packages/script/src/runtime/registry/google-tag-manager.ts

📝 Walkthrough

Walkthrough

The global Window augmentation in the Google Tag Manager registry now exposes only google_tag_manager. It no longer exposes the configurable dataLayer through GoogleTagManagerApi.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the primary change: removing the global Window.dataLayer declaration from the GTM registry.
Description check ✅ Passed The description directly explains the TypeScript compatibility issue, the proposed fix, and its impact.
Linked Issues check ✅ Passed The change addresses issue #852 by removing the conflicting global dataLayer member while preserving GoogleTagManagerApi and google_tag_manager.
Out of Scope Changes check ✅ Passed The single type declaration change is directly related to the linked issue and introduces no unrelated changes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Aug 7, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@nuxt/scripts@855

commit: e4a3758

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

GTM registry's global Window augmentation is irreconcilable with @gtm-support/core — unsuppressable TS2430 under TypeScript 7

1 participant