TypeScript 泛型高级用法:封装类型安全的请求工具函数

在 TypeScript 中,泛型(Generics)是实现类型安全的关键工具,尤其在处理网络请求时。通过高级泛型技术,我们可以封装一个请求工具函数,确保输入参数、响应数据和错误处理都严格类型化。这能显著减少运行时错误,提升代码可维护性。下面我将逐步构建一个类型安全的请求工具函数,涵盖泛型约束、条件类型和映射类型等高级用法。最终提供一个完整的实现(附类型定义)。

步骤 1: 理解需求和基本结构

我们需要一个工具函数,支持以下特性:

  • 指定请求方法(如 GET、POST)。
  • 类型化请求参数(如查询字符串或请求体)。
  • 类型化响应数据(通过泛型指定)。
  • 统一错误处理(类型化错误信息)。

基本函数签名可定义为:

async function request<T>(url: string, options: RequestOptions): Promise<T>

其中:

  • $T$ 是泛型参数,代表期望的响应数据类型。
  • RequestOptions 是自定义类型,用于封装请求配置。
步骤 2: 定义核心类型

使用 TypeScript 的高级泛型特性,定义以下类型来确保类型安全:

  • 请求参数类型:使用泛型约束和映射类型,处理不同 HTTP 方法(如 GET 无请求体,POST 有请求体)。
  • 响应类型:通过 $T$ 泛型动态指定。
  • 错误类型:自定义错误接口,统一处理异常。

类型定义如下:

// 定义错误类型,确保错误信息结构化
interface RequestError {
  message: string;
  status: number;
}

// 定义请求选项类型,使用泛型约束
type RequestOptions<D = unknown> = {
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE'; // 支持常见 HTTP 方法
  headers?: Record<string, string>; // 请求头类型化
  data?: D; // 请求体数据,泛型 D 指定其类型
};

// 定义响应类型:使用条件类型处理成功和失败
type ResponseResult<T> = T | RequestError;

步骤 3: 实现请求工具函数

结合泛型高级用法,我们实现函数:

  • 使用 fetch API 作为基础(确保浏览器兼容)。
  • 通过泛型 $T$ 和 $D$ 分别指定响应类型和请求体类型。
  • 添加错误处理逻辑,返回类型化错误。
async function request<T, D = unknown>(
  url: string,
  options: RequestOptions<D> = {}
): Promise<ResponseResult<T>> {
  try {
    const { method = 'GET', headers = {}, data } = options;
    const config: RequestInit = {
      method,
      headers: {
        'Content-Type': 'application/json',
        ...headers,
      },
    };

    // 处理请求体:仅当方法为 POST/PUT 且 data 存在时添加
    if ((method === 'POST' || method === 'PUT') && data !== undefined) {
      config.body = JSON.stringify(data);
    }

    const response = await fetch(url, config);
    if (!response.ok) {
      // 返回类型化错误
      const error: RequestError = {
        message: `Request failed with status ${response.status}`,
        status: response.status,
      };
      return error;
    }

    // 解析响应数据,并断言为泛型 T 类型
    const result: T = await response.json();
    return result;
  } catch (error) {
    // 捕获网络错误,统一类型化
    const defaultError: RequestError = {
      message: error instanceof Error ? error.message : 'Unknown error',
      status: 500,
    };
    return defaultError;
  }
}

步骤 4: 高级泛型用法解析
  • 泛型约束:在 RequestOptions<D> 中,$D$ 允许动态指定请求体类型,例如 $D$ 可以是用户定义的接口。
  • 条件类型ResponseResult<T> 使用联合类型,确保函数返回 $T$ 或 RequestError,便于调用者处理。
  • 默认泛型参数:$D = unknown$ 提供灵活性,当不传请求体时使用默认值。
  • 类型安全保证:通过 as T 断言(在真实场景中,应结合 Zod 或 io-ts 进行运行时验证),确保响应数据匹配 $T$。
步骤 5: 使用示例

调用工具函数时,传入泛型参数指定类型:

// 定义用户数据的接口
interface User {
  id: number;
  name: string;
}

// 示例 1: GET 请求,获取用户列表(响应类型为 User[])
async function fetchUsers(): Promise<User[] | RequestError> {
  return request<User[]>('https://api.example.com/users');
}

// 示例 2: POST 请求,创建用户(请求体类型为 User,响应类型为 User)
async function createUser(userData: User): Promise<User | RequestError> {
  return request<User, User>('https://api.example.com/users', {
    method: 'POST',
    data: userData, // 类型安全检查:userData 必须匹配 User 接口
  });
}

// 调用示例
(async () => {
  const users = await fetchUsers();
  if ('message' in users) {
    console.error('Error:', users.message); // 类型化错误处理
  } else {
    console.log('Users:', users); // users 是 User[] 类型
  }

  const newUser = await createUser({ id: 1, name: 'Alice' });
  if ('message' in newUser) {
    console.error('Error:', newUser.message);
  } else {
    console.log('Created user:', newUser); // newUser 是 User 类型
  }
})();

优势总结
  • 类型安全:泛型 $T$ 和 $D$ 确保请求和响应数据在编译时检查,减少运行时错误。
  • 可扩展性:通过高级泛型(如条件类型),轻松支持更多 HTTP 方法或自定义逻辑。
  • 错误处理统一RequestError 接口使错误信息结构化,便于调试。
  • 代码重用:封装后,可在整个项目中复用,提升开发效率。

此实现基于 TypeScript 4.x+,确保真实可靠。在实际项目中,建议结合验证库(如 Zod)增强运行时类型安全。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐