TypeScript 项目中实现类型安全的 API 请求与响应数据处理

5 阅读3分钟
# TypeScript 项目中实现类型安全的 API 请求与响应数据处理

## 问题背景
在 TypeScript 项目中,前端与后端通过 API 进行数据交互时,常常因接口返回结构变化或字段类型不一致导致运行时错误。虽然 TypeScript 提供了静态类型检查,但如果 API 请求和响应未正确建模,类型安全将大打折扣。因此,需要一套完整的机制来确保从请求参数到响应数据的全流程类型安全。

## 解决步骤

### 步骤1: 定义 API 接口的请求与响应类型
为每个 API 接口明确定义输入(请求参数)和输出(响应数据)的 TypeScript 类型。
```typescript
// types/api.ts
export interface User {
  id: number;
  name: string;
  email: string;
  createdAt: string; // ISO 字符串
}

export interface FetchUsersParams {
  page: number;
  limit: number;
  search?: string;
}

export interface ApiResponse<T> {
  success: boolean;
  data: T;
  message?: string;
}

预期结果:创建清晰、可复用的类型定义,为后续类型校验打下基础。

步骤2: 使用泛型封装 API 请求函数

封装一个通用的 request 函数,结合 fetchaxios,并通过泛型传递响应数据类型。

// lib/api-client.ts
import axios from 'axios';
import { ApiResponse } from '../types/api';

const client = axios.create({
  baseURL: 'https://api.example.com',
  timeout: 10000,
});

export const request = async <T>(config: {
  url: string;
  method: 'GET' | 'POST' | 'PUT' | 'DELETE';
  params?: any;
  data?: any;
}): Promise<ApiResponse<T>> => {
  try {
    const response = await client({
      ...config,
      params: config.params,
    });
    return {
      success: true,
      data: response.data,
      message: response.statusText,
    };
  } catch (error: any) {
    return {
      success: false,
      data: {} as T,
      message: error.response?.data?.message || error.message,
    };
  }
};

预期结果:request 函数能根据调用时传入的泛型自动推断返回的 data 类型,实现类型安全。

步骤3: 为具体 API 创建类型安全的调用函数

基于通用请求函数,封装具体业务接口,并绑定类型。

// api/user-api.ts
import { request } from '../lib/api-client';
import { User, FetchUsersParams, ApiResponse } from '../types/api';

export const fetchUsers = async (
  params: FetchUsersParams
): Promise<ApiResponse<User[]>> => {
  return request<User[]>({
    url: '/users',
    method: 'GET',
    params,
  });
};

预期结果:调用 fetchUsers 时,参数和返回值均有完整类型提示和校验,编辑器可自动补全并报错非法字段。

步骤4: 使用 zod 或 io-ts 实现运行时类型校验(最终方案)

静态类型在编译期有效,但无法防止后端返回非法数据。引入 zod 实现运行时校验,确保数据结构可信。

npm install zod
// schemas/user-schema.ts
import { z } from 'zod';

export const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  createdAt: z.string().datetime(),
});

export const UsersResponseSchema = z.object({
  success: z.boolean(),
  data: z.array(UserSchema),
  message: z.string().optional(),
});

export type User = z.infer<typeof UserSchema>;

更新请求函数以支持校验:

// lib/api-client-with-validation.ts
import { AxiosInstance } from 'axios';
import { parse } from 'valibot'; // 或使用 zod.parse

export const requestWithValidation = async <T>(
  client: AxiosInstance,
  url: string,
  schema: { parse: (data: any) => T },
  config: { method: string; params?: any; data?: any }
): Promise<T> => {
  const response = await client({ url, ...config });
  try {
    return schema.parse(response.data); // zod: schema.parse(response.data)
  } catch (error) {
    console.error('API 数据结构校验失败', error);
    throw new Error('后端返回数据格式异常');
  }
};

预期结果:即使后端返回错误结构数据,也能在运行时抛出明确错误,避免静默失败。

常见原因

  • 原因1: 后端接口文档滞后或未提供 OpenAPI 规范,导致前端类型定义不准确
  • 原因2: 仅依赖静态类型,未做运行时校验,生产环境出现字段缺失或类型错误
  • 原因3: 全局 any 泛滥,破坏了类型链路,导致类型安全形同虚设

预防措施

  1. 与后端约定使用 OpenAPI(Swagger)规范,通过工具(如 openapi-typescript)自动生成 TypeScript 类型
  2. 所有 API 调用必须使用泛型封装的请求方法,禁止直接使用 any
  3. 关键接口启用运行时校验(如 zod),尤其在 DTO 复杂或第三方接口场景
  4. 在 CI 流程中加入类型检查 tsc --noEmit,防止类型错误合入

注意事项

  1. 不要为了“快速开发”而使用 anyas any,这会破坏类型安全体系
  2. 注意日期字段的类型处理:后端通常返回字符串,前端需明确是 string 还是转换为 Date
  3. 在 TypeScript 配置中启用 "strict": true,确保类型检查全面生效