How to Fix TypeScript Errors for New Browser APIs
Fix TypeScript errors for new browser APIs with modern-web-types or a custom declaration. See how DOM types lag behind browsers and when to check support at runtime.
TypeScript reports “Property does not exist on type” for some new browser APIs because its built-in DOM types only include APIs that at least two browser engines support. You can fix it by swapping in a fuller DOM library or by declaring the missing member yourself.
You call an API that MDN documents and Chrome already runs, and the compiler tells you it doesn’t exist. Typing it as any makes the red line go away, but you lose type checking on that code.
This article explains why TypeScript’s DOM types trail behind browsers, then covers two type-safe fixes and the runtime check you still need with either one.
Key Takeaways
- TypeScript’s lib.dom.d.ts is generated by microsoft/TypeScript-DOM-lib-generator, which leaves out any API supported by fewer than two major browser engines.
- modern-web-types lowers that bar to one stable engine. Install it with
npm install --save-dev @typescript/lib-dom@npm:modern-web-types. - On TypeScript 6 or newer, the package only takes effect with
"libReplacement": truein compilerOptions. TypeScript 4.5 through 5.x needs no extra setting. - To add one missing member yourself, use
interface Windowin a global.d.tsfile. Atypealias will not merge. - Having types for an API does not mean the browser supports it, so single-engine APIs still need a check like
'fetchLater' in window.
What Does “Property Does Not Exist on Type” Mean?
TypeScript error TS2339, “Property does not exist on type”, appears when your code calls a real browser API that TypeScript’s bundled DOM types don’t declare. Your own code hasn’t added a global like window.myThing. The browser provides this method, but TypeScript’s types don’t know about it.
window.fetchLater('/analytics', { method: 'POST', activateAfter: 60_000 });
// error TS2339: Property 'fetchLater' does not exist on type 'Window & typeof globalThis'.
fetchLater() queues a request that the browser sends when the user leaves the page (it unloads or goes into the bfcache), or when the optional activateAfter timeout runs out, if that happens first. Philip Walton’s announcement of modern-web-types also uses element-scoped view transitions and the Long Animation Frame scripts property as examples of APIs hit by the same error.
Why Do TypeScript DOM Types Lag Behind Browsers?
TypeScript DOM types lag behind browsers on purpose. The TypeScript-DOM-lib-generator README sets the bar for inclusion at two or more of the major engines, Gecko (Firefox), Blink (Chrome/Edge), and WebKit (Safari), so that only APIs with real cross-vendor agreement get in. Every Chromium-based browser runs on Blink, so they all count as one engine.
Here is what that means in practice. An API that has shipped only in Blink can run in Chrome, Edge, and every other Chromium browser, and TypeScript will still say it doesn’t exist. The web-features entry for fetchLater lists only Chromium-based browsers as supporting it, so it doesn’t pass the two-engine rule.
The “Property does not exist on type” error reflects TypeScript’s inclusion policy. It doesn’t tell you whether the browser running your code supports the API.
Fix 1: Replace lib.dom With modern-web-types
modern-web-types is a package by Philip Walton that swaps out TypeScript’s DOM and WebWorker libraries without other changes to your code. As his announcement post explains, it is built with TypeScript’s own generator and w3c/webref data, but it includes any API that has shipped in one stable browser engine.
Install it under the @typescript/lib-dom alias. That is the name TypeScript looks for when it resolves its DOM library:
npm install --save-dev @typescript/lib-dom@npm:modern-web-types
On TypeScript 6 or newer, installing the alias does nothing until you also opt in to lib replacement:
{
"compilerOptions": {
"libReplacement": true
}
}
On TypeScript 4.5 through 5.x, leave this setting out. TypeScript 4.5 introduced the @typescript/lib-* lookup and applied it automatically. TypeScript 5.8 put it behind the libReplacement flag but left the flag on by default, and TypeScript 6 switched it off, as the modern-web-types pull request recommending lib replacement explains. According to the TypeScript 7.0 announcement, 7.0 adopts the 6.0 defaults, including libReplacement: false, so 7.0 also needs the flag.
Worker Projects
Worker code uses a separate entry point, added through types. Leave "WebWorker" out of lib, because the entry point replaces it:
{
"compilerOptions": {
"types": ["modern-web-types/webworker"],
"lib": ["ESNext"]
}
}
On TypeScript 5.x, setting types at all stops TypeScript from auto-including every @types/* package in node_modules. On TypeScript 6 and newer, types already defaults to [], so nothing is auto-included either way. In both cases, if you depend on packages such as @types/node, add them to the same array. The README also lists serviceworker, sharedworker, and audioworklet entry points.
Fix 2: Declare the Missing API With Interface Merging
If you only need one API, a small declaration file will do. Declare an interface with the same name as the built-in one, and TypeScript merges the declarations. It has to be an interface. A type Window = { ... } alias fails with a duplicate identifier error, because type aliases don’t merge.
The signature below follows MDN: fetchLater() accepts the same arguments as fetch() plus an activateAfter option, and it returns a FetchLaterResult whose activated boolean tells you whether the request has gone out yet.
// src/types/fetch-later.d.ts
interface DeferredRequestInit extends RequestInit {
activateAfter?: number;
}
interface FetchLaterResult {
readonly activated: boolean;
}
interface Window {
fetchLater(input: RequestInfo | URL, init?: DeferredRequestInit): FetchLaterResult;
}
This works without a declare global wrapper because a .d.ts file with no top-level import or export is a global script. Three rules keep it working:
- Keep the file non-module. If you add a top-level
importorexport, the file becomes a module. The interfaces stop merging globally, and you would needdeclare globalagain. - Make sure tsconfig covers it. The file has to match your
includepatterns. - Delete it when TypeScript ships the API. Once lib.dom declares the same members, your copies are redundant at best and can conflict at worst.
This declaration types window.fetchLater(...) only. Calling the bare global fetchLater(...) also needs a global function declaration, so stick to window.fetchLater in your code.
Do You Still Need Feature Detection?
Yes. TypeScript types describe what an API looks like. They don’t tell you which browsers have it. Both fixes make the compiler accept fetchLater, but browsers without it will still throw at runtime, so guard the call:
if ('fetchLater' in window) {
try {
window.fetchLater('/analytics', { method: 'POST', activateAfter: 60_000 });
} catch {
// quota exceeded: fall back or drop the beacon
}
} else {
// fall back, e.g. send the data immediately
}
The try block is there because MDN recommends catching QuotaExceededError around nearly every fetchLater() call. Treat single-engine APIs as progressive enhancement, and check support against your own browser targets on MDN’s fetchLater page.
Which Fix Should You Use?
| Approach | Setup | Coverage | When TypeScript catches up |
|---|---|---|---|
| modern-web-types | One install, one flag on TS 6+ | Every single-engine API | Nothing to remove |
Hand-written .d.ts | One small file per API | Only what you write | Delete the file |
as any / @ts-ignore | None | None | Hides real errors indefinitely |
Use modern-web-types if your project uses several new APIs or regularly adopts features before they reach other engines. Use a hand-written .d.ts if you need one or two members and would rather not replace the whole DOM library. Avoid as any and @ts-ignore: they turn off checking for the whole expression or line and will hide genuine mistakes later. Enforcing that with a TypeScript ESLint setup and the @typescript-eslint/no-explicit-any and @typescript-eslint/ban-ts-comment rules stops stray workarounds from building up.
Conclusion
The TypeScript “Property does not exist on type” error for new browser APIs comes from TypeScript’s two-engine rule, not from a mistake in your code or a missing "dom" entry. Replace lib.dom with modern-web-types if you use new APIs regularly, or merge a single interface into Window for a one-off, and keep a runtime check around every call. Start by checking your TypeScript version: if it’s 6 or newer, add "libReplacement": true when you install the package.
FAQs
Will installing @types/web fix missing types for new browser APIs?
Only for APIs that already meet the two-engine rule. @types/web is the same DOM library that ships inside TypeScript. It is generated by microsoft/TypeScript-DOM-lib-generator and published to npm, so it can deliver newer DOM types before the next TypeScript release. It applies the same inclusion policy, so a Chromium-only API like fetchLater stays missing. It installs through the same @typescript/lib-dom alias on TypeScript 4.5 and later, and on TypeScript 6 or newer it also needs libReplacement set to true.
Does adding DOM or ESNext to the lib option fix the error?
No. The lib option only chooses which of TypeScript's bundled declaration files get loaded. The bundled DOM file is generated under the two-engine rule, so adding 'DOM', 'DOM.Iterable', or 'ESNext' cannot add an API that file leaves out. ESNext adds JavaScript language features, not browser APIs. To fix the error, replace the DOM library with modern-web-types or declare the missing member in a global .d.ts file.
How do I declare a missing method on Element or HTMLElement instead of Window?
Merge into the interface that the specification defines the member on. In a global .d.ts file with no top-level import or export, declare interface Element with the new method. HTMLElement, SVGElement and the other element interfaces extend Element, so they all inherit it. Element-scoped startViewTransition() is defined on Element, so declaring it only on HTMLElement leaves SVG elements untyped. Take the exact signature from MDN or the spec.
Complete picture for complete understanding
Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.
Star on GitHub12k