TypeScript 如何从类型声明导入并重命名类型
结论
不能直接从 @types/firefox-webext-browser 导入并重命名 browser。该包提供全局声明,index.d.ts 不是模块,没有可供 import 的导出成员。
可以改用以下方案:
- 使用模块形式的
@types/webextension-polyfill提供类型。 - 仅引用类型,避免生成运行时
import。 - 在
browser.execute回调中为globalThis.browser创建局部别名。 - 将这个局部变量断言为 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 的执行上下文。
备注:内容仅供参考。
