Web Speech API 为浏览器提供了语音识别和语音合成两项能力,分别对应 SpeechRecognition 和 SpeechSynthesis 接口。在纯 JavaScript 环境中,直接调用 window.SpeechRecognition 或者 window.webkitSpeechRecognition 并不会遇到类型问题,但在 TypeScript 项目中,编译器会严格检查全局对象上是否存在这些属性。标准库 lib.dom.d.ts 对部分 Web Speech API 类型有定义,但浏览器厂商前缀版本和部分回调事件并没有被完整覆盖,这就导致我们不得不手动补齐类型声明。下面从实际开发角度出发,把语音识别与合成相关的类型定义完整梳理一遍。

为什么标准库没有覆盖完整
TypeScript 每次发布新版本时,会从 TypeScript-DOM-lib-generator 项目中生成 lib.dom.d.ts。这个生成器的数据源主要来自 Web 标准规范以及浏览器实现反馈,但 Web Speech API 的标准化进程比较曲折。语音识别的 SpeechRecognition 接口早在 2012 年就出现在 Chrome 中,并且长期使用 webkitSpeechRecognition 前缀暴露在全局对象上。而标准库倾向于只收录没有前缀的标准属性,对于厂商前缀版本往往采取忽略策略。
另一个原因在于 SpeechRecognition 的事件模型并非完全遵循标准 DOM 事件结构。比如 onresult 回调收到的事件对象是 SpeechRecognitionEvent,它继承自 Event,但额外包含了 resultIndex 和 results 属性。标准库中这些事件类型可能只给出了基础定义,缺少具体的事件映射,导致在添加事件监听时无法推断出回调参数的具体类型。语音合成部分的 SpeechSynthesisUtterance 同样存在 onboundary、onmark 等非标准事件,类型声明并不完整。
理解这一点后,我们就不能完全依赖 lib.dom.d.ts,而是需要通过自定义声明文件把自己用到的接口补充进去。好在 TypeScript 的声明合并机制允许我们在不修改标准库的前提下扩展全局类型。
定义语音识别相关类型
语音识别最核心的类型包括 SpeechRecognition 构造函数、SpeechRecognitionResultList、SpeechRecognitionResult 以及各类事件对象。下面这份声明覆盖了 Chrome 和 Edge 浏览器中 webkitSpeechRecognition 的实际行为。先定义结果类型,因为回调函数中会频繁访问这些结构。
interface SpeechRecognitionAlternative {
readonly transcript: string;
readonly confidence: number;
}
interface SpeechRecognitionResult {
readonly length: number;
item(index: number): SpeechRecognitionAlternative;
readonly isFinal: boolean;
[index: number]: SpeechRecognitionAlternative;
}
interface SpeechRecognitionResultList {
readonly length: number;
item(index: number): SpeechRecognitionResult;
[index: number]: SpeechRecognitionResult;
}
interface SpeechRecognitionEvent extends Event {
readonly resultIndex: number;
readonly results: SpeechRecognitionResultList;
}
interface SpeechRecognitionErrorEvent extends Event {
readonly error: string;
readonly message: string;
}上述类型定义了识别结果列表的索引签名,这样就能像访问数组一样获取对应索引的结果项。注意 isFinal 属性表示当前识别片段是否是最终结果,在实时语音输入过程中,中间结果可能多次变化,最终结果则不会再次更新。
接下来定义 SpeechRecognition 接口和其事件回调签名。这里使用接口声明而不是类声明,因为浏览器全局暴露的是构造函数对象,我们只需要描述实例属性即可。同时把 lang、continuous、interimResults 等常用配置项都包含进去。
interface SpeechRecognition extends EventTarget {
lang: string;
continuous: boolean;
interimResults: boolean;
maxAlternatives: number;
start(): void;
stop(): void;
abort(): void;
onaudiostart: ((this: SpeechRecognition, ev: Event) => any) | null;
onaudioend: ((this: SpeechRecognition, ev: Event) => any) | null;
onend: ((this: SpeechRecognition, ev: Event) => any) | null;
onerror: ((this: SpeechRecognition, ev: SpeechRecognitionErrorEvent) => any) | null;
onnomatch: ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => any) | null;
onresult: ((this: SpeechRecognition, ev: SpeechRecognitionEvent) => any) | null;
onsoundstart: ((this: SpeechRecognition, ev: Event) => any) | null;
onsoundend: ((this: SpeechRecognition, ev: Event) => any) | null;
onspeechstart: ((this: SpeechRecognition, ev: Event) => any) | null;
onspeechend: ((this: SpeechRecognition, ev: Event) => any) | null;
onstart: ((this: SpeechRecognition, ev: Event) => any) | null;
}
declare var SpeechRecognition: {
prototype: SpeechRecognition;
new(): SpeechRecognition;
};
declare var webkitSpeechRecognition: {
prototype: SpeechRecognition;
new(): SpeechRecognition;
};这里把 SpeechRecognition 和 webkitSpeechRecognition 都声明为构造函数变量,这样在全局作用域中就能直接使用这两个名字。事件回调属性中的 this 指向当前识别实例,回调参数根据事件类型区分开,比如 onerror 收到的是 SpeechRecognitionErrorEvent,而 onresult 收到的是包含结果的 SpeechRecognitionEvent。
如果只使用 webkitSpeechRecognition,还需要把它挂载到 Window 接口上。通过 declare global 进行接口合并,可以让 window.webkitSpeechRecognition 获得类型提示。在实际浏览器中,标准属性 window.SpeechRecognition 在 Chrome 71 之后也可用,但旧版本仍然只暴露 webkit 前缀,所以两个都声明为可选更稳妥。
declare global {
interface Window {
SpeechRecognition?: typeof SpeechRecognition;
webkitSpeechRecognition?: typeof webkitSpeechRecognition;
}
}定义语音合成相关类型
语音合成部分的类型定义相对简单,因为标准库已经包含了 SpeechSynthesis 接口和 SpeechSynthesisUtterance 的基础声明。不过部分浏览器的事件属性仍然需要手动合并才能获得准确类型。比如 onboundary 事件在合成过程中会返回单词边界信息,标准库并没有为它提供专门的 SpeechSynthesisBoundaryEvent 类型。
先定义边界事件和标记事件。边界事件的 charIndex 表示当前正在朗读的字符位置,name 表示边界类型。标记事件则由 utterance 中的文本标记触发,可以通过 mark 属性识别具体标记。
interface SpeechSynthesisBoundaryEvent extends Event {
readonly charIndex: number;
readonly charLength: number;
readonly elapsedTime: number;
readonly name: string;
}
interface SpeechSynthesisMarkEvent extends Event {
readonly charIndex: number;
readonly elapsedTime: number;
readonly mark: string;
}
interface SpeechSynthesisUtterance extends EventTarget {
lang: string;
pitch: number;
rate: number;
text: string;
voice: SpeechSynthesisVoice | null;
volume: number;
onboundary: ((this: SpeechSynthesisUtterance, ev: SpeechSynthesisBoundaryEvent) => any) | null;
onend: ((this: SpeechSynthesisUtterance, ev: Event) => any) | null;
onerror: ((this: SpeechSynthesisUtterance, ev: Event) => any) | null;
onmark: ((this: SpeechSynthesisUtterance, ev: SpeechSynthesisMarkEvent) => any) | null;
onpause: ((this: SpeechSynthesisUtterance, ev: Event) => any) | null;
onresume: ((this: SpeechSynthesisUtterance, ev: Event) => any) | null;
onstart: ((this: SpeechSynthesisUtterance, ev: Event) => any) | null;
}这里的 SpeechSynthesisUtterance 接口实际上是扩展标准库中已有的同名接口。由于 TypeScript 接口可以多次声明并自动合并,我们不必重新定义 text、voice 等基础属性,只需补充遗漏的事件回调类型。当然,为了演示完整性,这里也列出了常用属性,实际项目中可以在标准库已有声明的基础上只写增量部分。
需要注意的是 voice 属性类型为 SpeechSynthesisVoice | null,因为浏览器在未选择任何语音时该值为空。同时 pitch、rate、volume 都是数值类型,范围通常在 0 到 2 之间,其中 1 表示默认值。
使用声明文件并解决全局冲突
将上述代码保存为一个 web-speech.d.ts 文件,放入项目的 src 目录或 types 目录下,并确保它被 tsconfig.json 的 include 配置覆盖。这样 TypeScript 编译器就会自动加载这些全局声明,并在编辑器中提供完整的智能提示。
实际使用时,我们可以先判断浏览器暴露的是哪一个构造函数,再创建识别实例。下面这段代码展示了如何结合自定义类型进行语音识别初始化,同时监听 result 事件并获取转录文本。
const SpeechRecognitionImpl = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SpeechRecognitionImpl) {
throw new Error('当前浏览器不支持语音识别');
}
const recognition = new SpeechRecognitionImpl();
recognition.lang = 'zh-CN';
recognition.continuous = true;
recognition.interimResults = true;
recognition.onresult = (event) => {
let finalText = '';
for (let i = event.resultIndex; i < event.results.length; i++) {
const result = event.results[i];
if (result.isFinal) {
finalText += result[0].transcript;
}
}
console.log('最终识别文本:', finalText);
};
recognition.onerror = (event) => {
console.error('语音识别失败:', event.error, event.message);
};
recognition.start();这段代码中 event.results 的类型被正确推断为 SpeechRecognitionResultList,循环中访问 result[0].transcript 时也有完整提示,无需额外的类型断言。如果未定义自定义类型,编译器会报错说 window.SpeechRecognition 不存在,或者 onresult 的参数类型无法推断。
对于语音合成,使用方式同样简单。创建 SpeechSynthesisUtterance 实例,设置要朗读的文本,然后调用 speechSynthesis.speak 方法。类型声明保证了 utterance 上的事件回调参数类型准确,比如 onboundary 中可以安全访问 charIndex。
const utterance = new SpeechSynthesisUtterance('你好,世界');
utterance.lang = 'zh-CN';
utterance.rate = 1;
utterance.pitch = 1;
utterance.onboundary = (event) => {
console.log('当前朗读位置:', event.charIndex, '边界类型:', event.name);
};
utterance.onend = () => {
console.log('朗读完成');
};
window.speechSynthesis.speak(utterance);在自定义声明文件的加持下,以上代码全部通过类型检查。如果没有 SpeechSynthesisBoundaryEvent 的声明,回调参数 event 会被推断为普通 Event,访问 charIndex 时会报属性不存在错误。因此补齐类型定义不仅能消除编译错误,还能提高开发效率,减少运行时由于拼写错误带来的隐患。
另外,如果项目中只使用标准属性 window.SpeechRecognition,也可以不用声明 webkitSpeechRecognition,但为了兼容旧版 Chrome,建议同时保留。通过 typeof SpeechRecognition 引用构造函数类型,可以避免重复编写构造函数签名。这样当浏览器支持标准属性时,类型也不会产生冲突。
最后提醒一下,SpeechRecognition 接口中的 onresult 等回调属性在标准库中可能已经存在部分定义,自定义接口声明会与标准库进行合并,因此不会覆盖其他已有方法。这正是 TypeScript 声明合并的优势,让我们可以按需补充而不必从头开始写完整类型。将这份声明文件放进项目后,语音识别和合成相关的代码就会获得完整的类型支持,开发过程也会顺畅很多。

