Effective TypeScript

对函数表达式应用完整类型标注

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 的上下文类型推断机制。当一个函数表达式作为值赋给一个具有已知类型(如 DiceRollFntypeof fetch)的变量时,该已知类型会作为“上下文”信息,帮助 TypeScript 推断函数参数的类型,从而减少对函数参数进行显式标注的必要性。

这种机制在使用回调函数时尤为常见。例如,当将函数传递给 Array.prototype.mapfilter 等方法时,TypeScript 能够根据这些方法的回调签名自动推断回调参数的类型。

总结要点

  • 优先级: 当函数签名重复出现,或需要匹配另一函数的完整签名时,优先将类型标注应用于整个函数表达式。
  • 安全性: 使用 typeof fn 严格匹配签名,可以在函数实现内部捕获类型错误,提供更好的安全保障。
  • 简洁性: 这种方法允许 TypeScript 推断参数类型,减少冗余标注(DRY 原则)。
  • 例外: 对于单个的、独立的函数,传统的函数语句(Function Statement)是完全合适的。
在 GitHub 上编辑

上次更新于