Fix "Property does not exist on Window" in TypeScript
Learn how to safely extend the Window interface in TypeScript with declaration merging and declare global, and why bracket notation on an undeclared key fails under noImplicitAny.
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. Fix it by extending the Window interface using declaration merging — a declare global block in a module .d.ts file (one with export {};), or a top-level interface Window in a .d.ts file with no imports or exports, which TypeScript treats as a script in the global scope.
If that doesn't work, scroll to verify the fix — it also covers bracket notation (window['key']), and a .d.ts file or index.html script that tsc never checks.
What you'll see#
TS2339: Property 'myCustomProperty' does not exist on type 'Window & typeof globalThis'.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. It is a compile-time error, so it looks the same in any project where tsc checks the file with the DOM library loaded — a Next.js app, a Vite project, a Supabase client. In a Node.js-only project whose lib setting omits DOM, window itself is unknown and the error becomes TS2304: Cannot find name 'window'. instead (observed with TypeScript 5.9.3 and 7.0.2).
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 — declare the Window interface with standard web APIs only, not arbitrary custom properties. The interface is open to declaration merging (the fix relies on that), but until you add a declaration, your property is not part of it. 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:
// 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 (lib.dom.d.ts as shipped in TypeScript 5.9.3) as:
// From lib.dom.d.ts, TypeScript 5.9.3 (excerpt)
interface Window {
get location(): Location;
set location(href: string);
alert(message?: any): void;
// ... many standard properties
[index: number]: Window;
}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 they are untangled here before 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, but they name three different things.
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, and it works the same way for other global interfaces such as Document — 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.
Add export {}; (or any import or export) to these .d.ts files so TypeScript treats them as modules and honours the declare global block. Skip that line and the augmentation does nothing. With skipLibCheck: false, tsc says so: TS2669: Augmentations for the global scope can only be directly nested in external modules or ambient module declarations. With skipLibCheck: true — set in the tsconfig.json that create-next-app generates — that error is hidden and the only symptom is the original TS2339 at every use site (both observed with TypeScript 5.9.3 and 7.0.2).
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.
A common trap is reaching for type Window = ... instead. Type aliases cannot be re-opened to add new properties; interfaces can. So whenever you plan to augment a global, use interface, never type.
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.”
// 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#
- Create a new file in your project — e.g.,
src/types/window.d.ts. The.d.tsextension tells TypeScript this is a type declaration file. - Paste the snippet above, replacing
myCustomPropertyandstringwith your actual property name and type. - Save the file. TypeScript automatically picks up
.d.tsfiles in yoursrcdirectory (notsconfig.jsonchanges needed). - Now assign and read
window.myCustomProperty— no more errors.
Committing this file early means every later window property has an obvious home.
Here’s how it looks in practice:
// 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:
npx tsc --noEmitYou should see no errors for window.myCustomProperty assignments or reads. Running the same command as a CI step catches a regression before it reaches a pull request.
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 this, with noImplicitAny on (strict turns it on), in TypeScript 5.9.3 and 7.0.2:
TS7015: Element implicitly has an 'any' type because index expression is not of type 'number'.This happens because the only index signature Window has is a numeric one — [index: number]: Window; in lib.dom.d.ts, for child frames such as window[0]. A string key that is not a declared property matches neither, so the access falls back to an implicit any, which noImplicitAny rejects (TS7015 in the compiler's message catalogue). A string index signature ([key: string]: any) would silence it — but you shouldn’t add one.
The fix is still declaration merging. Extend Window with your known keys, and use dot notation:
// src/types/window.d.ts
export {};
declare global {
interface Window {
analyticsEnabled: boolean;
}
}
// src/lib/analytics.ts
// ✅ 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 — The .d.ts file or the script is outside what tsc checks#
tsc only checks the files its tsconfig.json puts in the program: patterns in include pick up .ts, .tsx and .d.ts files (plus .js and .jsx with allowJs). Two cases follow:
- An inline
<script>inindex.htmlis never type-checked bytsc, so no.d.tsfile applies to it. Move the script into a TypeScript file compiled bytsc. - A
.d.tsfile outside theincludepatterns — for example a roottypes/window.d.tswhileincludeis["src/**/*"]— is never loaded, so every use site keeps theTS2339error (observed with TypeScript 5.9.3 and 7.0.2). Add the file explicitly intsconfig.json:
{
"include": ["types/window.d.ts", "src/**/*"]
}If you use Next.js, the tsconfig.json that create-next-app generates already includes **/*.ts, so a declaration in src/types/window.d.ts is picked up without changes, as long as you have not narrowed include.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
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, two practices help:
-
Centralise window extensions — declare all custom
windowproperties in a single.d.tsfile (e.g.,src/types/window.d.ts). This makes them easy to audit, review, and update. -
Keep the escape hatches visible — leave
noImplicitAnyon (strictenables it): it is what turnswindow['undeclaredKey']intoTS7015instead of a silentany. If you lint with typescript-eslint,no-explicit-anyflags(window as any)casts, andno-unsafe-member-accessflags member access on a value typedany; the latter needs type-aware linting.
For typing the rest of a Next.js and Supabase codebase — generated database types, Zod validation and type-safe queries — see the Type Safety Guide for Next.js + Supabase in TypeScript.
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.
Further reading: Fix Cannot find name 'console' in TypeScript (TS2584), TypeScript Getter Setter Errors: TS1056, TS2378, TS2380 Fix. Also see Fix Zod 4: errors Does Not Exist on ZodError. Also see Fix TS1084 Invalid 'reference' directive in @types/node.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
Fix Zod 4: errors Does Not Exist on ZodError
Repair the missing errors property without losing nested paths, hiding validation failures or turning unexpected exceptions into form errors.
Fix Cannot find name 'console' in TypeScript (TS2584)
console.log fails to type-check in a Node project. See which two tsconfig settings hide console, why the compiler hint points the wrong way, and the fix.
Fix TS2305: Module Has No Exported Member in TypeScript
TS2305 says the export you're importing doesn't exist under that name. The cause is almost always one of: a typo, default-vs-named confusion, a CJS/ESM interop mismatch, or @types drifting from the runtime package. Each has a precise fix.


