PAYATHON 2026

TypeScript 如何从类型声明导入并重命名类型

技术老齐

结论

不能直接从 @types/firefox-webext-browser 导入并重命名 browser。该包提供全局声明,index.d.ts 不是模块,没有可供 import 的导出成员。

可以改用以下方案:

  1. 使用模块形式的 @types/webextension-polyfill 提供类型。
  2. 仅引用类型,避免生成运行时 import。
  3. 在 browser.execute 回调中为 globalThis.browser 创建局部别名。
  4. 将这个局部变量断言为 WebExtension browser 类型。

这样不会把 WebExtension 的 browser 声明添加到全局作用域,也不会干扰 WebdriverIO 的 browser 或 wdio.conf.ts。

安装模块形式的类型声明

npm install --save-dev @types/webextension-polyfill

只需安装类型声明,无需安装或加载 webextension-polyfill 的运行时代码。实际的 browser 对象仍由 Firefox 扩展环境提供。

推荐写法

type WebExtensionBrowser = typeof import('webextension-polyfill')

await browser.execute(
  async (url: string) => {
    const { browser: webextBrowser } = globalThis as unknown as {
      browser: WebExtensionBrowser
    }

    await webextBrowser.bookmarks.create({ url })
  },
  url
)

这里的名称各有用途:

  • 回调外部的 browser 是 WebdriverIO browser。
  • 回调内部的 globalThis.browser 是 Firefox 扩展环境提供的 WebExtension browser。
  • webextBrowser 是回调内部使用的局部别名。

WebExtensionBrowser 只存在于类型系统中,编译后会被删除。传给 browser.execute 的函数不会依赖 Node.js 侧的导入,也不会因为外部变量不存在而触发 ReferenceError。

也可以在回调中直接赋值:

type WebExtensionBrowser = typeof import('webextension-polyfill')

await browser.execute(
  async (url: string) => {
    const webextBrowser = (
      globalThis as unknown as { browser: WebExtensionBrowser }
    ).browser

    await webextBrowser.bookmarks.create({ url })
  },
  url
)

原来的导入方式为什么不可行

下面的代码有两个问题:

import { browser as webextBrowser } from '@types/firefox-webext-browser'

通常不应直接从 @types/... 路径导入。@types 包由 TypeScript 的类型解析机制加载,代码中的模块名一般应使用对应的实际模块名。

此外,@types/firefox-webext-browser 声明的是全局 browser,没有模块导出。即使改用 import type,也无法从非模块声明文件中取得命名导出:

// 仍然不可行
import type { browser as WebextBrowser } from '@types/firefox-webext-browser'

import type 只能让导入在编译后被删除,无法把全局声明文件转换成模块。

Triple-Slash Directive 为什么仍会冲突

下面的引用会把 firefox-webext-browser 的全局声明加入整个 TypeScript 编译程序:

/// <reference types="firefox-webext-browser" />

这个引用不会为当前文件创建独立的类型作用域。只要 wdio.conf.ts 和测试文件属于同一个 TypeScript program,全局 browser 声明就可能参与其他文件的类型解析,并与 WebdriverIO 的全局声明冲突。

下面的代码还会带来运行时问题:

export const webextBrowser = browser

它会生成一个真实的 JavaScript 变量。browser.execute 通常会把回调发送到浏览器端执行,却不会自动带上回调外部的模块变量。浏览器端可能因此找不到 webextBrowser,并抛出:

ReferenceError: webextBrowser is not defined

因此,传给 browser.execute 的回调不应依赖外部运行时别名。类型可以定义在回调外部,因为编译时会将其擦除;实际使用的变量应在回调内部创建。

只声明当前需要的 API

如果不想增加 @types/webextension-polyfill,可以在测试代码中定义一个最小接口:

interface WebExtensionBrowser {
  bookmarks: {
    create(details: {
      url?: string
      title?: string
      parentId?: string
      index?: number
    }): Promise<unknown>
  }
}

await browser.execute(
  async (url: string) => {
    const { browser: webextBrowser } = globalThis as unknown as {
      browser: WebExtensionBrowser
    }

    await webextBrowser.bookmarks.create({ url })
  },
  url
)

这种写法可以避开全局冲突,但接口需要自行维护,也无法自动获得完整的 WebExtension API 类型检查。如果测试会用到多个 browser.* API,使用现成的模块类型声明通常更方便。

注意事项

browser.execute 中的代码必须运行在拥有 bookmarks 权限的扩展上下文中。如果执行环境是普通网页,即使通过了 TypeScript 类型检查,运行时也可能没有 globalThis.browser.bookmarks。

类型断言只会告诉 TypeScript 当前对象的类型。它不会创建 browser、添加扩展权限,也不会改变 WebdriverIO 的执行上下文。

备注:内容仅供参考。