在基于浏览器的 HID 设备交互场景里,WebHID 协议把设备内部的报告描述符解析成集合树,而 TypeScript 内置的 HIDCollectionInfo 类型仅仅把 usagePage 和 usage 标注为 number。这种宽泛处理让开发者面对键盘、鼠标、游戏手柄等不同设备时,不得不手动记住 0x01 配 0x06 代表键盘,0x01 配 0x02 代表鼠标,代码里散落着大量魔法数字。更合适的方式是用枚举、字面量类型和联合类型将这些十六进制数值建模成语义化数据类型,在读取集合信息时直接获得类型提示和自动补全。本文将围绕 WebHID Collection API 的用途页展开,给出可落地的 TypeScript 定义方案。

先厘清用途页与集合类型
HID 报告描述符通过用途页(Usage Page)和用途(Usage)共同确定一个控制项的功能。用途页是个 16 位值,高字节通常用作页标识,低字节描述具体用途。比如用途页 0x01 是通用桌面页(Generic Desktop Page),在该页下 0x06 表示键盘、0x02 表示鼠标、0x04 表示摇杆;用途页 0x09 是按钮页,页内任意非零用途都代表一个按钮;用途页 0x0C 是消费控制页,包含音量、播放、浏览器等功能键。WebHID 在 HIDCollectionInfo 中直接暴露了 usagePage 和 usage,但并未提供这些数值的语义映射。
集合本身也有类型约束,浏览器使用的 HIDCollectionType 枚举区分了物理集合、应用集合、逻辑集合、报告集合等。一个设备可能有多个集合,例如带键盘和鼠标的复合设备会同时包含应用集合(Application Collection)和物理集合(Physical Collection)。在写 TypeScript 类型时,除了约束用途页的数值范围,还应该保留集合层级和子集合关系,否则过滤输入报告和输出报告时会丢失上下文。理解这些原始字段是定义数据类型的前提,下一步就是把常用页转成可读枚举。
为常见用途页建立枚举与联合类型
先定义用途页枚举,使用普通 enum 即可保证运行时值和类型名共存。下面列出最常用的几个页面,数值来自 HID Usage Tables 规范。
enum UsagePage {
GenericDesktop = 0x01,
Keyboard = 0x07,
LED = 0x08,
Button = 0x09,
Consumer = 0x0C,
Digitizer = 0x0D,
VendorDefinedStart = 0xFF00,
VendorDefinedEnd = 0xFFFF,
}
接着给每个页面下常用的用途编号命名。通用桌面页可以这样写:
enum GenericDesktopUsage {
Pointer = 0x01,
Mouse = 0x02,
Joystick = 0x04,
GamePad = 0x05,
Keyboard = 0x06,
Keypad = 0x07,
MultiAxisController = 0x08,
SystemControl = 0x80,
}
消费控制页里的用途数量较多,可以只挑选业务需要用到的部分:
enum ConsumerUsage {
Volume = 0xE0,
Mute = 0xE2,
PlayPause = 0xCD,
ScanNextTrack = 0xB5,
ScanPreviousTrack = 0xB6,
BrowserHome = 0x223,
}
此时可以把 HIDCollectionInfo 包装成一个更精确的类型。下面使用可辨识联合区分不同用途页,键名 usagePage 是判别字段,usage 的类型随页面收窄,这样在 switch 分支里就能拿到具体用途的字面量。
type GenericDesktopCollection = {
usagePage: UsagePage.GenericDesktop;
usage: GenericDesktopUsage;
type: HIDCollectionType;
children: HIDCollectionInfo[];
inputReports: HIDReportInfo[];
outputReports: HIDReportInfo[];
featureReports: HIDReportInfo[];
};
type ConsumerCollection = {
usagePage: UsagePage.Consumer;
usage: ConsumerUsage;
type: HIDCollectionType;
children: HIDCollectionInfo[];
inputReports: HIDReportInfo[];
outputReports: HIDReportInfo[];
featureReports: HIDReportInfo[];
};
type KnownHIDCollection = GenericDesktopCollection | ConsumerCollection;
上面代码中的 HIDCollectionType 和 HIDReportInfo 来自浏览器内置的 DOM 类型声明。如果项目使用较旧版本的 TypeScript,可能缺失部分 WebHID 类型,需要手动声明 interface HIDCollectionInfo 和 interface HIDDevice,或者安装最新版 @types/w3c-web-hid。建议优先升级 TypeScript 版本,让内置类型与新规范保持一致。
在读取设备集合时应用类型守卫
浏览器返回的 device.collections 类型是 HIDCollectionInfo[],其中 usagePage 是 number,不会自动识别成前面定义的 KnownHIDCollection。因此需要编写类型守卫函数,对原始数值做运行时检查,只有落在枚举范围内才收窄为自定义类型。下面是一个针对通用桌面页的守卫:
function isGenericDesktopCollection(
collection: HIDCollectionInfo
): collection is GenericDesktopCollection {
return collection.usagePage === UsagePage.GenericDesktop
&& Object.values(GenericDesktopUsage).includes(
collection.usage as GenericDesktopUsage
);
}
这里用 Object.values 检查 collection.usage 是否属于枚举值,从而避免直接断言导致的误判。如果设备厂商使用自定义用途,不属于任何已知枚举,守卫会返回 false,不会破坏后续逻辑。遍历设备集合时,可以先用守卫过滤已知类型,再按具体功能分发:
async function handleHIDDevice(device: HIDDevice) {
const collections = device.collections;
if (!collections) return;
for (const collection of collections) {
if (isGenericDesktopCollection(collection)) {
switch (collection.usage) {
case GenericDesktopUsage.Keyboard:
console.log('检测到键盘集合,准备读取按键报告');
break;
case GenericDesktopUsage.Mouse:
console.log('检测到鼠标集合,准备读取位移报告');
break;
case GenericDesktopUsage.GamePad:
console.log('检测到游戏手柄集合,准备读取摇杆报告');
break;
default:
console.log('其他通用桌面用途', collection.usage);
}
} else if (collection.usagePage === UsagePage.Consumer) {
// 可以继续扩展消费者控制集合的处理
console.log('发现消费控制集合,usage =', collection.usage);
} else {
console.log('未识别的集合,usagePage =', collection.usagePage);
}
}
}
这段代码展示了类型守卫和分派逻辑的组合。由于 isGenericDesktopCollection 已经将 collection 收窄为 GenericDesktopCollection,在 switch 中 collection.usage 会获得对应的字面量联合类型,编辑器可以给出补全提示,也避免了把鼠标用途和键盘用途混用的错误。对于消费控制页,如果业务里需要精确的 usage 类型,可以按照同样模式补一个 isConsumerCollection 守卫。
处理厂商自定义用途与可维护性
HID 规范把 0xFF00 到 0xFFFF 之间的用途页保留给厂商自定义,很多条形码扫描器、专业测量设备、工业控制器都会使用这个区间。面对这类设备,强类型枚举无法穷举所有可能,但可以保留一个通用的厂商拓展类型,把 usagePage 约束在 UsagePage.VendorDefinedStart 到 UsagePage.VendorDefinedEnd 范围内,usage 继续用 number 并配合注释说明。这样既不会滥用 as any,又给未知设备留出了扩展口。
另外,如果你在多处重复 inputReports、outputReports 等字段,会显得啰嗦。可以用泛型基类提取公共结构,再让具体集合类型继承:
interface BaseHIDCollectionInfo {
type: HIDCollectionType;
children: HIDCollectionInfo[];
inputReports: HIDReportInfo[];
outputReports: HIDReportInfo[];
featureReports: HIDReportInfo[];
}
type GenericDesktopCollection = BaseHIDCollectionInfo & {
usagePage: UsagePage.GenericDesktop;
usage: GenericDesktopUsage;
};
这种方式比复制粘贴字段更易维护,也适用于新增用途页。定义好类型后,建议在团队中把 KnownHIDCollection 作为统一出口,所有与集合相关的函数签名都使用它,而不是原生的 HIDCollectionInfo。长期来看,当 WebHID 规范更新或 TypeScript 内置类型变化时,你只需要调整自己的联合类型和守卫函数,业务代码几乎不用改动。
WebHID 目前在 Chromium 内核浏览器中可用,实际项目中还应该处理用户授权、设备断开重连、报告读写失败等情况。本文聚焦于用途页数据类型,为这些后续交互提供了清晰的类型基础。只要把集合识别逻辑封装好,后续读取输入报告时就能根据用途页直接知道数据是按键盘按键解析还是按鼠标位移解析,从根源上减少类型不匹配带来的调试成本。
WebHID Collection APITypeScript类型定义用途页数据类型修改时间:2026-09-21 16:29:35