You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
dist/runtime/registry/google-tag-manager.d.ts augments the global Window unconditionally:
exportinterfaceGoogleTagManagerApi{google_tag_manager: GoogleTagManagerInstancedataLayer: DataLayer&{push: DataLayerPush}// β required}declare global {interfaceWindowextendsGoogleTagManagerApi{}}
@gtm-support/core (the engine behind @gtm-support/vue-gtm, a widely used Vue/Nuxt GTM integration) declares the same member as optional:
// @gtm-support/core@3.0.1 lib/index.d.tsdeclare global {interfaceWindow{dataLayer?: DataLayerObject[]}}
Interface declarations merge, so Window.dataLayer becomes DataLayerObject[] | undefined, which then fails the extends GoogleTagManagerApi clause this module itself added:
error TS2430: Interface 'Window' incorrectly extends interface 'GoogleTagManagerApi'.
Types of property 'dataLayer' are incompatible.
Type 'DataLayerObject[] | undefined' is not assignable to type 'DataLayer & { push: DataLayerPush; }'.
Type 'undefined' is not assignable to type 'DataLayer & { push: DataLayerPush; }'.
Type 'undefined' is not assignable to type 'DataLayer'.
Three things make this hard to live with:
It fires even if you never touch GTM via this module. We use useScriptGoogleAnalytics and have never called useScriptGoogleTagManager β the augmentation ships regardless, because the registry composable's auto-import pulls the .d.ts into the program.
It is unsuppressable from the consumer side. The error is reported at ourdeclare global { interface Window { β¦ } } sites, one per augmentation block, not at either dependency. With skipLibCheck: true a dependency-vs-dependency conflict is silently ignored; it only materialises because we also augment Window.
tsc doesn't warn you β it just picks a winner.tsc@5.9.3 exits 0 here, because it does not verify merged interfaces against their extends bases. But the type it resolves is the optional one, so GoogleTagManagerApi's contract quietly loses:
window.dataLayer.push({event: 'x'})// ~~~~~~~~~ TS18048: 'window.dataLayer' is possibly 'undefined'
TypeScript 7 / tsgodoes perform that check, which is how we found this. So this will surface for many more people as TS 7 adoption grows β it is not a tsgo bug.
π οΈ Minimal reproduction
Three files, strict: true, skipLibCheck: true, moduleResolution: "Bundler", lib: ["ESNext","DOM"]:
This is arguably more accurate independently of the conflict: window.dataLayer is created by the GTM snippet at load time, so before the script loads it genuinely is undefined. It would also make the two declarations compatible, since optional-vs-optional merges cleanly.
Alternatives, if changing the public type is undesirable:
Keep GoogleTagManagerApi as-is but drop the declare global { interface Window extends GoogleTagManagerApi {} } block. The type is already exported and used as useScriptGoogleTagManager<T extends GoogleTagManagerApi>, so the global augmentation is what creates the collision without being needed for the composable's own typing.
Present in 0.13.2 too, with a byte-identical declaration β this is long-standing, not a recent regression.
The required dataLayer was added in fix(googleTagManager): add missing dataLayer typeΒ #388 ("fix(googleTagManager): add missing dataLayer type"), so I assume it is deliberate; the coexistence case just doesn't seem to have come up.
Workaround we're using meanwhile: a types-only pnpm patch removing the global augmentation, since we don't call useScriptGoogleTagManager. Happy to open a PR for whichever direction you prefer.
π The bug
dist/runtime/registry/google-tag-manager.d.tsaugments the globalWindowunconditionally:@gtm-support/core(the engine behind@gtm-support/vue-gtm, a widely used Vue/Nuxt GTM integration) declares the same member as optional:Interface declarations merge, so
Window.dataLayerbecomesDataLayerObject[] | undefined, which then fails theextends GoogleTagManagerApiclause this module itself added:Three things make this hard to live with:
useScriptGoogleAnalyticsand have never calleduseScriptGoogleTagManagerβ the augmentation ships regardless, because the registry composable's auto-import pulls the.d.tsinto the program.declare global { interface Window { β¦ } }sites, one per augmentation block, not at either dependency. WithskipLibCheck: truea dependency-vs-dependency conflict is silently ignored; it only materialises because we also augmentWindow.tscdoesn't warn you β it just picks a winner.tsc@5.9.3exits 0 here, because it does not verify merged interfaces against theirextendsbases. But the type it resolves is the optional one, soGoogleTagManagerApi's contract quietly loses:tsgodoes perform that check, which is how we found this. So this will surface for many more people as TS 7 adoption grows β it is not atsgobug.π οΈ Minimal reproduction
Three files,
strict: true,skipLibCheck: true,moduleResolution: "Bundler",lib: ["ESNext","DOM"]:tsgo/tsc@7.0.2βTS2430atours.tstsc@5.9.3β exits 0 (and typeswindow.dataLayeras possibly-undefined)ours.tsβtsgoalso exits 0, becauseskipLibCheckhides the lib-vs-lib conflictπ‘ Suggested fix
Make
dataLayeroptional inGoogleTagManagerApi:export interface GoogleTagManagerApi { google_tag_manager: GoogleTagManagerInstance - dataLayer: DataLayer & { push: DataLayerPush } + dataLayer?: DataLayer & { push: DataLayerPush } }This is arguably more accurate independently of the conflict:
window.dataLayeris created by the GTM snippet at load time, so before the script loads it genuinely isundefined. It would also make the two declarations compatible, since optional-vs-optional merges cleanly.Alternatives, if changing the public type is undesirable:
GoogleTagManagerApias-is but drop thedeclare global { interface Window extends GoogleTagManagerApi {} }block. The type is already exported and used asuseScriptGoogleTagManager<T extends GoogleTagManagerApi>, so the global augmentation is what creates the collision without being needed for the composable's own typing.βΉοΈ Additional context
@nuxt/scripts@1.3.2,@gtm-support/vue-gtm@3.2.0β@gtm-support/core@3.0.1, Nuxt 4.4.8,vue-tsc3.x.0.13.2too, with a byte-identical declaration β this is long-standing, not a recent regression.dataLayerwas added in fix(googleTagManager): add missing dataLayer typeΒ #388 ("fix(googleTagManager): add missing dataLayer type"), so I assume it is deliberate; the coexistence case just doesn't seem to have come up.pnpm patchremoving the global augmentation, since we don't calluseScriptGoogleTagManager. Happy to open a PR for whichever direction you prefer.