Fix "Property does not exist on Window" in TypeScript
TypeScript

Fix "Property does not exist on Window" in TypeScript

Learn how to safely extend the Window interface in TypeScript using declaration merging, type assertions, and bracket notation to avoid compile-time errors.

2026-06-17
9 min read
Fix "Property does not exist on Window" in TypeScript

TL;DR#

If you’re seeing Property 'myCustomProperty' does not exist on type 'Window & typeof globalThis', the cause is that TypeScript’s built-in DOM type definitions don’t include your custom property on window. In my experience, this is the single most common global-scoping error in TypeScript projects — and almost always the same root cause. Fix it by extending the Window interface using declaration merging — declare a global module or ambient namespace that adds your property to the Window interface.

If that doesn’t work, scroll to verify the fix — there are two common variants this guide also covers.

What you'll see#

plaintext
TS2339: Property 'myCustomProperty' does not exist on type 'Window & typeof globalThis'.

I hit this exact error the first time I tried to attach an analytics flag to window in a Next.js app — and I’ve watched at least a dozen teammates hit the same wall since. It happens when you try to assign a property directly on window, like window.myCustomProperty = 'value', or access it later as window.myCustomProperty, in a TypeScript file. The behavior is the same across Node.js environments, browsers, and frameworks like Next.js or Supabase — wherever TypeScript compiles your code.

TypeScript enforces strict type safety on the global window object, which is typed as Window & typeof globalThis. Unless your property is declared in the DOM lib types (e.g., localStorage, location, document), the compiler treats it as an unknown property and raises a compile-time error.

Root cause#

TypeScript’s DOM type definitions — shipped in lib.dom.d.ts — describe the Window interface as a closed type. It includes only standard web APIs, not arbitrary custom properties. When you assign window.myCustomProperty = 'value', TypeScript checks whether myCustomProperty exists on the Window interface. Since it doesn’t, the compiler throws TS2339.

The relevant code path is:

ts
// src/lib/my-feature.ts
// ❌ This fails at compile time
window.myCustomProperty = 'someValue';
 
// ❌ This also fails — reading the property
const value = window.myCustomProperty;

The underlying issue is that TypeScript’s type system is structural and static. It does not allow runtime property assignment on objects unless the property is explicitly declared in the type definition. This is intentional: it prevents typos, enforces consistency, and catches bugs before runtime.

The Window interface is defined in the TypeScript DOM library as:

ts
// From lib.dom.d.ts (simplified)
interface Window {
  readonly location: Location;
  alert(message?: string): void;
  // ... many standard properties
}

There’s no myCustomProperty here — and TypeScript won’t guess or infer it. It expects you to declare your extension explicitly. To do that cleanly, you’ll lean on three TypeScript features that get conflated all the time, so let me untangle them before showing the fix.

The three concepts you need: declaration merging, global augmentation, interface extension#

These three terms show up interchangeably in blog posts and Stack Overflow answers, which is half the reason engineers struggle with this error. In my own projects I keep them mentally separate, and that has saved me hours of debugging.

Declaration merging#

TypeScript declaration merging is the compiler’s behavior of combining multiple declarations that share the same name into a single definition. When you write interface Window { ... } in your own .d.ts file, TypeScript doesn’t replace the built-in Window interface from lib.dom.d.ts — it merges yours with the existing one. The resulting Window type has all the original DOM properties plus whatever you added.

This isn’t a hack. It’s a documented feature designed for exactly this scenario. I’ve relied on declaration merging for years to extend Window, Document, Array, and Request — all without forking the lib types.

Global augmentation#

Global augmentation is the mechanism that lets you add to globally-scoped types like Window, Document, or globalThis from inside a module file. The declare global { ... } block tells TypeScript: “I’m inside a module, but I want to extend the global scope.” Without this wrapper, your interface declaration would be scoped to the file alone and have no effect on window.

In every project I work on, I always add export {}; at the top of these .d.ts files so TypeScript treats them as modules and honors the declare global block. Skip that line and the augmentation silently does nothing — a frustrating failure mode I’ve debugged more than once.

Interface extension#

Interface extension in TypeScript comes in two flavors. The first is the familiar interface X extends Y syntax. The second — the one we use here — relies on the fact that interfaces are open-ended: you can write two separate interface Window { ... } declarations in different files and TypeScript merges them automatically.

The trap I see engineers fall into is reaching for type Window = ... instead. Type aliases are closed; they don’t merge. So whenever you plan to augment a global, always use interface, never type. That single rule has prevented countless “why isn’t this working” moments on my teams.

The fix#

Now that the concepts are clear, the fix is short. The correct way to add custom properties to window in TypeScript is to extend the Window interface using declaration merging. This tells TypeScript: “I know this property exists at runtime — please treat it as valid.”

ts
// src/types/window.d.ts (or any .d.ts file in your project)
export {};
 
declare global {
  interface Window {
    myCustomProperty: string;
  }
}

That single change addresses the cause because declaration merging allows you to augment existing interfaces at compile time. TypeScript merges your declaration with the built-in Window interface, so window.myCustomProperty becomes a known property with type string.

Step by step#

  1. Create a new file in your project — e.g., src/types/window.d.ts. The .d.ts extension tells TypeScript this is a type declaration file.
  2. Paste the snippet above, replacing myCustomProperty and string with your actual property name and type.
  3. Save the file. TypeScript automatically picks up .d.ts files in your src directory (no tsconfig.json changes needed).
  4. Now assign and read window.myCustomProperty — no more errors.

I usually commit this file at the start of a project so it’s there before anyone needs it. Retro-fitting it across a 200-file repo is a chore.

Here’s how it looks in practice:

ts
// src/types/window.d.ts
export {};
 
declare global {
  interface Window {
    analyticsEnabled: boolean;
    debugMode: boolean;
    featureFlags: Record<string, boolean>;
  }
}
 
// src/lib/analytics.ts
// ✅ No type errors — TypeScript now knows these exist
window.analyticsEnabled = true;
window.debugMode = false;
window.featureFlags = { newCheckout: true, darkMode: false };

Note the export {}; line. It forces the file to be treated as a module, which is required for declare global to work in TypeScript. Without it, the declare global block may be ignored.

Verify the fix#

Run:

bash
npx tsc --noEmit

You should see no errors for window.myCustomProperty assignments or reads. I run this as the first step in CI on every TypeScript project I touch — it catches regressions before they hit a PR.

If you’re still seeing the error, two common variants exist:

Variant A — Using bracket notation without proper indexing#

You might try window['analyticsEnabled'] = true and still get:

plaintext
TS7017: Element implicitly has an 'any' type because type 'Window' has no index signature.

This happens because Window has no index signature (e.g., [key: string]: any). TypeScript doesn’t allow arbitrary string indexing on Window unless you add one — but you shouldn’t.

The fix is still declaration merging. Extend Window with your known keys, and use dot notation:

ts
// src/types/window.d.ts
declare global {
  interface Window {
    analyticsEnabled: boolean;
  }
}
 
// ✅ Now this works — and is type-safe
window.analyticsEnabled = true;

Avoid window['analyticsEnabled'] unless you need dynamic keys — and even then, prefer a typed map or object.

Variant B — Assigning in a non-module file (e.g., index.html script)#

If you’re adding a property in a <script> tag inside index.html, TypeScript won’t see your .d.ts file unless it’s included in tsconfig.json.

The fix is to either:

  • Move the script into a TypeScript file compiled by tsc, or
  • Add your .d.ts file explicitly in tsconfig.json:
json
{
  "include": ["src/types/window.d.ts", "src/**/*"]
}

Or, if using Next.js, place the declaration in src/types/window.d.ts and ensure it’s in the include path — Next.js compiles all .ts/.d.ts files under src.

Why this happens (and how to avoid it next time)#

TypeScript enforces strict type checking on window because it’s a global object shared across your entire application. Allowing arbitrary properties without declaration would defeat the purpose of type safety — you could accidentally overwrite window.location, or misspell window.localStorage as window.localStorge and never know until runtime.

To prevent regressions, adopt two practices I’ve used across multiple production codebases:

  1. Centralize window extensions — declare all custom window properties in a single .d.ts file (e.g., src/types/window.d.ts). This makes it easy to audit, review, and update.

  2. Use ESLint rules — enforce that all window accesses are type-safe. Install @typescript-eslint/eslint-plugin and enable @typescript-eslint/no-explicit-any and @typescript-eslint/no-unsafe-member-access. Add a custom rule to disallow bracket notation on window unless it’s a known key:

json
{
  "rules": {
    "@typescript-eslint/no-unsafe-member-access": "error",
    "no-undef": "off",
    "no-unused-vars": "off"
  }
}

I cover this in detail in Complete Type Safety Guide for Next.js and Supabase with TypeScript, including how to type window properties used in middleware or server-side rendering.

That covers the diagnosis, the three concepts behind the fix, the implementation, the verification, and the long-term guardrails. Once you’ve centralized your Window augmentation in a single .d.ts file and wired up the ESLint rules above, this error class quietly disappears from your codebase — and the next engineer who hits it will have a much shorter path to the fix than you did.

Frequently Asked Questions

|

Have more questions? Contact us

Written by

Mahdi Br
Mahdi Br

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote

One email a month — no fluff

RLS gotchas, Next.js cache debugging, and the one Supabase setting that bit me last month.