Playwright CDPSession 类型定义是 CDP 协议的子集,运行正常却 tsc 报错
写浏览器自动化脚本时接触到了 Playwright 的 CDPSession,用来调一些高级 CDP 命令——screencast 推流、拦截文件选择弹窗、IME 文本输入。代码在浏览器里跑得好好的,截帧数据也能拿到,但 tsc --noEmit 一跑就炸:
error TS2345: Argument of type '"Page.startScreencast"' is not assignable
to parameter of type 'keyof CommandParameters'.
error TS2345: Argument of type '"Page.handleFileChooser"' is not assignable
to parameter of type 'keyof CommandParameters'.
反直觉:运行时完全正常,只有编译器报错。
现象
以下这批 CDP 命令,在 Chromium 里都能正确响应,但全部触发 TS2345:
| 命令 | 用途 |
|------|------|
| Page.startScreencast / Page.stopScreencast | viewport JPEG 推流 |
| Page.screencastFrameAck | 帧确认 |
| Page.setInterceptFileChooserDialog | 拦截 <input type=file> |
| Page.handleFileChooser | 给被拦截的 chooser 回填路径 |
| Emulation.setDeviceMetricsOverride | 动态修改 viewport / DPR |
| Input.insertText | IME 路径写文字(中文输入法等) |
cdp.on('Page.screencastFrame', ...) 也一样,事件名同样不在 typed 列表里。
根因
查 node_modules/playwright/types/protocol.d.ts,CDPSession.send 的签名是:
send<T extends keyof CommandParameters>(
method: T,
params?: CommandParameters[T]
): Promise<CommandReturnValues[T]>CommandParameters 是一个精选清单——Playwright 团队只把他们自己内部用到的 CDP 命令加进去了,并不是 Chrome DevTools Protocol 的全集。这个设计让 IDE 有自动补全,但代价是大量运行时有效的命令在类型层面"不存在"。
Chrome DevTools Protocol 官方文档里有几百个命令,但 Playwright 的 CommandParameters 只收录了其中一部分。缺失的命令在 Chromium 里能正常执行,却会让 TypeScript 编译失败,让人以为是 API 不支持,实际上只是类型没有覆盖。
正解
方案 A(推荐):在函数顶部做一次 AnyCdp 转型
import type { CDPSession } from 'playwright'
type AnyCdp = {
send: (method: string, params?: any) => Promise<any>
on: (event: string, cb: (e: any) => void) => void
}
export async function setupScreencast(cdp: CDPSession, onFrame: (data: string) => void) {
const c = cdp as unknown as AnyCdp // 只转型一次
await c.send('Page.startScreencast', {
format: 'jpeg',
quality: 70,
maxWidth: 1280,
maxHeight: 720,
everyNthFrame: 2,
})
c.on('Page.screencastFrame', (frame) => {
onFrame(frame.data)
c.send('Page.screencastFrameAck', { sessionId: frame.sessionId }).catch(() => {})
})
}要点:
as unknown as AnyCdp——双层 cast,绕过 TypeScript 对不兼容类型的检查- 只在函数入口 cast 一次赋给局部变量
c,后续所有 send/on 都用c,不要在每个调用处分别 cast - 对外暴露的函数参数仍是
CDPSession,调用方不受影响 - 返回值是
Promise<any>,需要特定字段时再按实际 CDP 文档做局部类型断言
封装成独立函数,入参仍是 CDPSession,函数内部 const c = cdp as unknown as AnyCdp 做一次转型,之后所有 CDP 调用都走 c。既消灭了 TS2345,又把逃逸范围控制到最小,lint 也不会抱怨。
方案 B:每处单独 as any(不推荐)
await (cdp.send as any)('Page.startScreencast', { ... })每个 call site 都要写,散在各处,eslint 容易告警,IDE 跳转也丢失。
方案 C:扩展 Commands 接口(重量级)
// types/cdp-extras.d.ts
declare module 'playwright' {
interface Commands {
'Page.startScreencast': { params: { format: 'jpeg' | 'png'; quality?: number } }
// ...
}
}能拿回完整类型提示,但要手抄 CDP protocol.json,升级 Playwright 时可能需要维护。只有项目大量直击底层 CDP 时才值得。
额外的运行时坑
-
screencast 在页面导航后会断:主框架
framenavigated之后Page.startScreencast的订阅会失效,需要重新 send 开启。Playwright 不会自动重启推流。 -
Page.handleFileChooser必须配合setInterceptFileChooserDialog: enabled=true:如果没有先开拦截,handleFileChooser静默无效,不报错,路径也填不进去。 -
Input.insertText走 IME 路径:不会触发keydown/keyup事件。需要按键事件的场景仍要用Input.dispatchKeyEvent。 -
Emulation.setDeviceMetricsOverride是动态修改 viewport 的唯一途径:通过browser.newContext({ viewport })只能在创建时设置,已开 context 想改尺寸只能走 CDP。
验证
npx tsc --noEmit
# 应输出 0 errors一句话外卖
Playwright 的
CDPSession.send类型是内部精选子集,不是 CDP 全集——编译报错不代表命令无效,用as unknown as一次性转型就够,不要怀疑协议本身。