在Web应用中读取设备的加速度计、陀螺仪或环境光传感器数据时,Generic Sensor API是目前的标准方案。创建传感器实例时可以传入一个配置对象,其中的frequency属性决定了每秒采样的次数。这个值看起来只是一个普通的数字,但在TypeScript中如何为它定义类型,却直接关系到代码能否在编译期拦截非法参数、能否准确表达规范语义。本文将从规范出发,一步步给出严谨且实用的类型定义方案。

一、Generic Sensor API中采样频率的规范约定
按照W3C的Generic Sensor API规范,以加速度计为例,构造函数接受一个可选的配置对象:
const sensor = new Accelerometer({
frequency: 60, // 每秒采样60次,即60Hz
referenceFrame: 'device'
});frequency在规范中被定义为double类型,表示期望的采样频率,单位是Hz。需要注意的是,规范并没有强制要求浏览器精确按这个值采样,实际频率由设备能力决定。通过传感器实例的frequency只读属性可以拿到真实生效的值,它可能与你传入的值不同。例如传入60,设备可能只支持50Hz,实际返回的就是50。
另一个容易踩坑的点是边界值:传入0或负数时,规范规定传感器将不会产生任何读数,而传入超出设备上限的值会被自动钳制到设备支持的最大频率。此外,部分浏览器(主要是桌面Chrome)在没有真实传感器的环境下会直接抛出异常。这些行为上的细节,正是我们在设计类型时需要考虑的约束来源。
二、从number到字面量联合类型的演进
最直接的定义方式是照搬规范:
interface SensorOptions {
frequency?: number;
}这种写法虽然正确,但约束太弱。任何number都能通过编译,包括-1、NaN甚至Infinity,这些值传给浏览器要么静默失效要么直接抛错,问题被推迟到了运行时。对于内部封装传感器逻辑的项目来说,通常只会使用几档固定的频率,比如界面动效用60Hz、低功耗后台监测用10Hz。这种情况下,字面量联合类型是更好的选择:
type SensorFrequency = 10 | 20 | 50 | 60;
interface StrictSensorOptions {
frequency?: SensorFrequency;
}
// 编译通过
const a: StrictSensorOptions = { frequency: 60 };
// 编译报错:Type '30' is not assignable to type 'SensorFrequency'
const b: StrictSensorOptions = { frequency: 30 };字面量联合类型的优势在于错误暴露得极早,IDE还能给出自动补全提示。它的缺点同样明显:频率档位是硬编码的,换一款设备支持的频率集合就不同了,类型定义和运行时现实容易脱节。因此这种方案适合传感器型号固定、逻辑封闭的场景,比如针对特定硬件的PWA应用。
折中的做法是保留number,但用模板字面量类型配合范围校验函数表达约束意图。TypeScript本身不支持“0到1000之间的数字”这种依赖类型的表达(数字字面量类型只能列举),所以更务实的路径是把类型定义和运行时守卫结合起来。
三、品牌类型加运行时校验的完整方案
品牌类型(branded type)是TypeScript社区表达“带额外约束的原始类型”的经典技巧。思路是给number交叉一个不可见的标记属性,使得这个类型只能通过指定的工厂函数产生,从而保证凡是类型为ValidFrequency的值,一定经过了运行时校验:
declare const FrequencyBrand: unique symbol;
type ValidFrequency = number & { readonly [FrequencyBrand]: true };
function createFrequency(hz: number): ValidFrequency {
if (!Number.isFinite(hz) || hz <= 0) {
throw new RangeError(`采样频率必须是正的有限数值,收到的是 ${hz}`);
}
if (hz > 1000) {
console.warn(`频率 ${hz}Hz 超出常见设备上限,将被浏览器钳制`);
}
return hz as ValidFrequency;
}
interface BrandedSensorOptions {
frequency?: ValidFrequency;
}
// 正确用法:必须经过校验函数
const freq = createFrequency(60);
const sensor = new Accelerometer({ frequency: freq });这个方案的价值在于把校验逻辑收敛到唯一入口。任何绕过createFrequency直接构造ValidFrequency的写法都会在编译期被拒绝,而校验函数内部可以随时扩展规则,比如检查是否为整数、是否落在设备白名单内。类型层面的品牌标记只是编译期的幻影,运行时不会有任何额外开销,因为符号属性从未真正挂到数字上。
如果不想引入品牌的复杂度,类型守卫配合类型谓词也能达到类似效果,而且写法更轻:
function isValidFrequency(hz: unknown): hz is number {
return typeof hz === 'number'
&& Number.isFinite(hz)
&& hz > 0;
}
function parseFrequency(raw: unknown): number {
if (!isValidFrequency(raw)) {
throw new TypeError('非法的采样频率');
}
return raw;
}实际项目中还应注意读取传感器实例上真实的frequency属性来做后续逻辑判断,而不是假定传入值就是生效值。配合start()与onerror事件处理,才能完整覆盖传感器不可用、权限被拒绝等异常分支。综合来看,简单的业务用字面量联合类型即可,跨设备的通用封装则推荐品牌类型加运行时校验的组合,两者都能让采样频率这份数据在类型系统里得到应有的地位。
TypeScriptGeneric Sensor API传感器采样频率修改时间:2026-09-03 04:46:33