12k
All articles

如何修复新浏览器 API 引发的 TypeScript 错误

使用 modern-web-types 或自定义声明修复新版浏览器 API 的 TypeScript 错误。了解 DOM 类型滞后的原因,以及如何在运行时检测支持情况。

OpenReplay Team
OpenReplay Team
如何修复新浏览器 API 引发的 TypeScript 错误

对于某些新的浏览器 API,TypeScript 会报告“Property does not exist on type”,原因是其内置的 DOM 类型只收录至少两个浏览器引擎支持的 API。你可以通过换用更完整的 DOM 库,或自行声明缺失的成员来解决这一问题。

你调用了一个 MDN 有文档、Chrome 也已经能运行的 API,编译器却告诉你它不存在。把它标注为 any 可以让红色波浪线消失,但这段代码也就失去了类型检查。

本文将解释 TypeScript 的 DOM 类型为何落后于浏览器,然后介绍两种类型安全的修复方法,以及无论采用哪种方法都仍然需要的运行时检查。

要点速览

  • TypeScript 的 lib.dom.d.ts 由 microsoft/TypeScript-DOM-lib-generator 生成,该工具会排除支持它的主流浏览器引擎少于两个的 API。
  • modern-web-types 将这一门槛降低为一个稳定版引擎。安装命令为 npm install --save-dev @typescript/lib-dom@npm:modern-web-types。
  • 在 TypeScript 6 及更高版本中,只有在 compilerOptions 中设置 "libReplacement": true,该包才会生效。TypeScript 4.5 至 5.x 无需额外设置。
  • 如需自行添加某个缺失的成员,请在全局 .d.ts 文件中使用 interface Window。type 别名无法合并。
  • 某个 API 有类型定义并不代表浏览器支持它,因此对于仅单一引擎支持的 API,仍需进行类似 'fetchLater' in window 的检查。

“Property Does Not Exist on Type”是什么意思?

当你的代码调用了一个真实存在、但 TypeScript 内置 DOM 类型中未声明的浏览器 API 时,就会出现 TypeScript 错误 TS2339,即“Property does not exist on type”。这并不是因为你的代码自行添加了类似 window.myThing 的全局变量。这个方法由浏览器提供,只是 TypeScript 的类型定义并不知道它的存在。

window.fetchLater('/analytics', { method: 'POST', activateAfter: 60_000 });
// error TS2339: Property 'fetchLater' does not exist on type 'Window & typeof globalThis'.

fetchLater() 会将一个请求加入队列,浏览器会在用户离开页面时(页面卸载或进入 bfcache)发送该请求;如果设置了可选的 activateAfter 超时且先到期,则会在超时到期时发送。Philip Walton 在发布 modern-web-types 的文章中,还将元素级视图过渡(view transitions)和 Long Animation Frame 的 scripts 属性列为会遇到同样错误的 API 示例。

为什么 TypeScript 的 DOM 类型落后于浏览器?

TypeScript 的 DOM 类型落后于浏览器是有意为之。TypeScript-DOM-lib-generator 的 README 将收录门槛设定为至少有两个主流引擎支持,这些引擎包括 Gecko(Firefox)、Blink(Chrome/Edge)和 WebKit(Safari),以确保只有真正获得跨厂商共识的 API 才会被收录。所有基于 Chromium 的浏览器都运行在 Blink 上,因此它们只算作一个引擎。

这在实践中意味着:一个仅在 Blink 中发布的 API,可以在 Chrome、Edge 以及其他所有 Chromium 浏览器中运行,但 TypeScript 仍会认为它不存在。web-features 中 fetchLater 的条目显示只有基于 Chromium 的浏览器支持它,因此它未能满足双引擎规则。

“Property does not exist on type”错误反映的是 TypeScript 的收录策略,它并不能说明运行你代码的浏览器是否支持该 API。

修复方法 1:用 modern-web-types 替换 lib.dom

modern-web-types 是 Philip Walton 开发的一个包,它可以替换 TypeScript 的 DOM 和 WebWorker 库,而无需对你的代码做其他改动。正如他在发布文章中所述,该包使用 TypeScript 自己的生成器和 w3c/webref 数据构建,但会收录任何已在一个稳定版浏览器引擎中发布的 API。

请以 @typescript/lib-dom 别名安装它,这是 TypeScript 解析其 DOM 库时查找的名称:

npm install --save-dev @typescript/lib-dom@npm:modern-web-types

在 TypeScript 6 及更高版本中,仅安装该别名不会产生任何效果,你还需要显式启用 lib 替换:

{
  "compilerOptions": {
    "libReplacement": true
  }
}

在 TypeScript 4.5 至 5.x 中,请不要添加此设置。TypeScript 4.5 引入了 @typescript/lib-* 查找机制并自动应用;TypeScript 5.8 将其置于 libReplacement 标志之后,但该标志默认开启;而 TypeScript 6 将其默认关闭,详见 modern-web-types 中建议启用 lib 替换的 pull request。根据 TypeScript 7.0 发布公告,7.0 沿用了 6.0 的默认值,包括 libReplacement: false,因此 7.0 同样需要设置该标志。

Worker 项目

Worker 代码使用单独的入口点,通过 types 添加。请不要在 lib 中包含 "WebWorker",因为该入口点会取而代之:

{
  "compilerOptions": {
    "types": ["modern-web-types/webworker"],
    "lib": ["ESNext"]
  }
}

在 TypeScript 5.x 中,一旦设置了 types,TypeScript 就不会再自动引入 node_modules 中的所有 @types/* 包。在 TypeScript 6 及更高版本中,types 的默认值已经是 [],所以无论如何都不会自动引入任何包。在这两种情况下,如果你依赖 @types/node 之类的包,都需要将它们添加到同一个数组中。README 中还列出了 serviceworker、sharedworker 和 audioworklet 入口点。

修复方法 2:通过接口合并声明缺失的 API

如果你只需要一个 API,一个小型声明文件就足够了。声明一个与内置接口同名的接口,TypeScript 就会合并这些声明。这里必须使用 interface。type Window = { ... } 别名会因标识符重复而报错,因为类型别名无法合并。

下面的签名遵循 MDN 的描述:fetchLater() 接受与 fetch() 相同的参数,外加一个 activateAfter 选项,并返回一个 FetchLaterResult,其中的 activated 布尔值表示请求是否已经发出。

// 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;
}

这种写法无需 declare global 包裹,因为没有顶层 import 或 export 的 .d.ts 文件是全局脚本。要让它持续有效,需遵循三条规则:

  • 保持文件为非模块。 如果添加了顶层 import 或 export,该文件就会变成模块,接口将不再进行全局合并,你需要重新使用 declare global。
  • 确保 tsconfig 覆盖该文件。 该文件必须匹配你的 include 模式。
  • TypeScript 收录该 API 后删除此文件。 一旦 lib.dom 声明了相同的成员,你的副本往好了说是多余的,往坏了说可能引发冲突。

此声明仅为 window.fetchLater(...) 提供类型。若要直接调用全局的 fetchLater(...),还需要额外声明一个全局函数,因此在代码中请坚持使用 window.fetchLater。

还需要特性检测吗?

需要。TypeScript 类型描述的是 API 的形态,并不能告诉你哪些浏览器支持它。两种修复方法都能让编译器接受 fetchLater,但不支持它的浏览器在运行时仍会抛出错误,因此请为调用加上保护:

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
}

之所以使用 try 块,是因为 MDN 建议在几乎所有 fetchLater() 调用外捕获 QuotaExceededError。请将仅单一引擎支持的 API 视为渐进增强,并对照 MDN 的 fetchLater 页面,根据你自己的目标浏览器检查支持情况。

应该选择哪种修复方法?

方法配置覆盖范围TypeScript 跟进收录后
modern-web-types一次安装,TS 6+ 需设置一个标志所有单一引擎支持的 API无需移除任何内容
手写 .d.ts每个 API 一个小文件仅限你自行编写的部分删除该文件
as any / @ts-ignore无无无限期地掩盖真实错误

如果你的项目使用了多个新 API,或经常在功能被其他引擎支持之前就率先采用,请使用 modern-web-types。如果你只需要一两个成员,且不想替换整个 DOM 库,请使用手写的 .d.ts。避免使用 as any 和 @ts-ignore:它们会关闭整个表达式或整行的检查,日后会掩盖真正的错误。借助 TypeScript ESLint 配置以及 @typescript-eslint/no-explicit-any 和 @typescript-eslint/ban-ts-comment 规则来强制执行这一约定,可以防止零散的变通写法不断累积。

结论

新浏览器 API 引发的 TypeScript“Property does not exist on type”错误源于 TypeScript 的双引擎规则,而不是你的代码有误或缺少 "dom" 配置项。如果你经常使用新 API,请用 modern-web-types 替换 lib.dom;如果只是偶尔使用,可以向 Window 合并单个接口。同时,请在每次调用时保留运行时检查。第一步是确认你的 TypeScript 版本:如果是 6 或更高版本,安装该包时请添加 "libReplacement": true。

常见问题

安装 @types/web 能修复新浏览器 API 缺失类型的问题吗?

仅适用于已满足双引擎规则的 API。@types/web 与 TypeScript 内置的 DOM 库是同一个库,它由 microsoft/TypeScript-DOM-lib-generator 生成并发布到 npm,因此可以在下一个 TypeScript 版本发布之前提供更新的 DOM 类型。但它采用相同的收录策略,所以像 fetchLater 这样仅 Chromium 支持的 API 依然缺失。在 TypeScript 4.5 及更高版本中,它通过同样的 @typescript/lib-dom 别名安装;在 TypeScript 6 及更高版本中,还需要将 libReplacement 设置为 true。

在 lib 选项中添加 DOM 或 ESNext 能修复该错误吗?

不能。lib 选项只决定加载 TypeScript 内置的哪些声明文件。内置的 DOM 文件是按照双引擎规则生成的,因此添加 'DOM'、'DOM.Iterable' 或 'ESNext' 都无法补上该文件中缺失的 API。ESNext 添加的是 JavaScript 语言特性,而非浏览器 API。要修复该错误,请用 modern-web-types 替换 DOM 库,或在全局 .d.ts 文件中声明缺失的成员。

如何在 Element 或 HTMLElement 而非 Window 上声明缺失的方法?

合并到规范中定义该成员的那个接口。在一个没有顶层 import 或 export 的全局 .d.ts 文件中,声明包含新方法的 interface Element。HTMLElement、SVGElement 以及其他元素接口都继承自 Element,因此它们都会继承该方法。元素级的 startViewTransition() 定义在 Element 上,如果只在 HTMLElement 上声明,SVG 元素就会缺少类型。请从 MDN 或规范中获取准确的签名。

Open-source session replay

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

We use cookies to improve your experience. By using our site, you accept cookies.