对函数表达式应用完整类型标注
Item 12 建议在可能的情况下,将类型标注应用于整个函数表达式(Function Expressions),而非仅仅标注函数参数和返回类型。这种做法的核心是利用 TypeScript 的类型推断能力,减少冗余的类型声明,并提供更强大的类型安全检查,尤其是在需要匹配现有函数签名时,。
函数语句与函数表达式
TypeScript 对函数语句(Function Statement)和函数表达式(Function Expression)进行了区分。此建议主要针对函数表达式,例如赋值给变量的匿名函数或箭头函数。
将类型应用于整个表达式,意味着将类型签名标注在用于存储该表达式的变量上:
// 函数类型别名定义
type DiceRollFn = (sides: number) => number;
// 应用类型到整个函数表达式
const rollDice: DiceRollFn = sides => {
// TypeScript 能够推断 sides 的类型为 number
return Math.floor(Math.random() * sides) + 1;
};核心优势
1. 减少类型重复 (DRY 原则)
当代码中存在多个具有相同函数签名的函数时,提取一个函数类型别名(例如 BinaryFn)可以大幅减少类型标注的重复,使得类型信息与函数实现逻辑分离,代码实现更加简洁。
代码示例:合并函数签名
type BinaryFn = (a: number, b: number) => number;
// 通过 BinaryFn,TypeScript 推断 a 和 b 的类型为 number
const add: BinaryFn = (a, b) => a + b;
const sub: BinaryFn = (a, b) => a - b;
// 这种方式同时获得了对所有函数表达式返回类型为 number 的检查。2. 匹配现有签名并增强类型安全
将类型应用于整个函数表达式,可以确保该函数的类型签名与预期目标(如另一个函数的签名)完全匹配。这在重写或包装现有函数时尤其有用。
通过使用 typeof 操作符,可以精确匹配另一个函数的完整类型签名,从而确保参数类型一致,并防止实现错误泄漏。
代码示例:使用 typeof 匹配签名
假设我们要编写一个与浏览器内置 fetch 函数签名完全匹配的 checkedFetch 函数,并增加状态检查:
declare function fetch(
input: RequestInfo, init?: RequestInit,
): Promise<Response>;
// 使用 typeof fetch 匹配完整的签名
const checkedFetch: typeof fetch = async (input, init) => {
// input 和 init 的类型被自动推断
const response = await fetch(input, init);
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
return response;
};如果实现中发生错误,例如不小心返回了一个 Error 对象而非抛出异常,TypeScript 将在函数实现内部立即捕获错误,因为返回的类型不再匹配 fetch 预期的 Promise<Response> 类型:
const checkedFetch: typeof fetch = async (input, init) => {
const response = await fetch(input, init);
if (!response.ok) {
return new Error('Request failed: ' + response.status);
}
return response;
};
// TypeScript 报错:类型 'Promise<Response | Error>' 不可赋值给类型 'Promise<Response>'。这种在实现内部发现错误的方式,比在调用 checkedFetch 的代码中才暴露问题更安全、更简洁。
进阶应用:参数匹配与修改返回类型
如果需要匹配现有函数的参数类型,但同时需要修改返回类型,可以使用 Rest 参数和内置的 Parameters 实用工具类型。Parameters<T> 工具类型能够提取函数类型 T 的参数列表为一个元组类型。
代码示例:修改返回类型
// 定义一个参数与 fetch 相同,但返回 Promise<number> 的函数
async function fetchANumber(
...args: Parameters<typeof fetch> // 获取 fetch 的参数元组类型
): Promise<number> {
const response = await checkedFetch(...args);
const num = Number(await response.text());
return num;
}
// 检查器推断 fetchANumber 的参数列表与 fetch 相同。上下文类型推断
将类型应用于整个函数表达式,可以看作是利用了 TypeScript 的上下文类型推断机制。当一个函数表达式作为值赋给一个具有已知类型(如 DiceRollFn 或 typeof fetch)的变量时,该已知类型会作为“上下文”信息,帮助 TypeScript 推断函数参数的类型,从而减少对函数参数进行显式标注的必要性。
这种机制在使用回调函数时尤为常见。例如,当将函数传递给 Array.prototype.map 或 filter 等方法时,TypeScript 能够根据这些方法的回调签名自动推断回调参数的类型。
总结要点
- 优先级: 当函数签名重复出现,或需要匹配另一函数的完整签名时,优先将类型标注应用于整个函数表达式。
- 安全性: 使用
typeof fn严格匹配签名,可以在函数实现内部捕获类型错误,提供更好的安全保障。 - 简洁性: 这种方法允许 TypeScript 推断参数类型,减少冗余标注(DRY 原则)。
- 例外: 对于单个的、独立的函数,传统的函数语句(Function Statement)是完全合适的。
上次更新于