TypeScript泛型约束与条件类型在API SDK类型安全设计中的实战应用

TypeScript泛型在API SDK中的核心作用

TypeScript泛型是构建类型安全API SDK的核心工具。在前端开发中,API请求的请求参数和响应数据类型往往各不相同,泛型允许在保持类型检查的前提下实现通用的请求封装。泛型约束和条件类型进一步增强了类型系统的表达力,能够精确描述API请求与响应之间的类型映射关系,避免运行时类型错误。

类型安全的API SDK设计目标:编译时捕获参数类型错误,避免传入错误的请求参数;自动推断响应数据类型,减少手动类型断言;支持RESTful风格的路径参数类型推断;对不同HTTP方法提供不同的参数约束。

基础泛型请求封装

使用泛型封装fetch请求,实现请求和响应的类型安全:

// 基础请求参数类型
interface RequestOptions {
  headers?: Record<string, string>;
  timeout?: number;
  signal?: AbortSignal;
}

// API响应包装类型
interface ApiResponse<T> {
  code: number;
  message: string;
  data: T;
}

// 泛型请求函数
async function request<T>(
  url: string,
  options: RequestInit & RequestOptions = {}
): Promise<ApiResponse<T>> {
  const { headers, timeout = 10000, signal, ...init } = options;

  const controller = new AbortController();
  const timeoutId = setTimeout(() => controller.abort(), timeout);

  try {
    const response = await fetch(url, {
      ...init,
      headers: { 'Content-Type': 'application/json', ...headers },
      signal: signal ?? controller.signal,
    });

    if (!response.ok) {
      throw new Error(`HTTP ${response.status}: ${response.statusText}`);
    }

    return await response.json() as ApiResponse<T>;
  } finally {
    clearTimeout(timeoutId);
  }
}

// 使用示例:类型自动推断
interface User {
  id: number;
  name: string;
  email: string;
}

// T 被推断为 User
const result = await request<User>('/api/users/1');
// result.data 的类型为 User,IDE自动补全
console.log(result.data.name);  // 类型安全访问

泛型约束与条件类型实现RESTful路径推断

RESTful API的路径参数类型可以通过泛型约束和条件类型实现编译时推断,避免手动拼接字符串时出现参数遗漏或类型错误:

// 路径参数提取类型
type ExtractParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? { [K in Param | keyof ExtractParams<Rest>]: string | number }
    : T extends `${string}:${infer Param}`
    ? { [K in Param]: string | number }
    : {};

// 条件类型:检查路径是否包含参数
type HasParams<T extends string> = T extends `${string}:${string}` ? true : false;

// 路径替换函数(带类型约束)
function buildPath<T extends string>(
  path: T,
  ...args: HasParams<T> extends true
    ? [ExtractParams<T>]
    : []
): string {
  if (args.length === 0) return path;

  const params = args[0] as Record<string, string | number>;
  return path.replace(/:(\w+)/g, (_, key) => String(params[key] ?? ''));
}

// 使用示例
// 无参数路径
const path1 = buildPath('/api/users');
// 有参数路径,类型系统强制要求传入参数对象
const path2 = buildPath('/api/users/:userId/posts/:postId', {
  userId: 123,
  postId: 456,
});
// 编译错误:缺少参数
// buildPath('/api/users/:userId'); // Error: Argument of type '[]' is not assignable

HTTP方法约束与请求体类型映射

不同HTTP方法对请求参数的约束不同,GET请求不需要body,POST/PUT请求需要body。通过条件类型和泛型约束实现方法级别的类型安全:

// HTTP方法类型
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';

// 请求配置:根据方法类型条件性地要求body
type RequestConfig<M extends HttpMethod, B = unknown> =
  M extends 'GET' | 'DELETE'
    ? { method: M; params?: Record<string, unknown> }
    : { method: M; body: B; params?: Record<string, unknown> };

// API客户端类
class ApiClient {
  private baseUrl: string;

  constructor(baseUrl: string) {
    this.baseUrl = baseUrl;
  }

  // 泛型方法:R为响应类型,B为请求体类型
  async call<M extends HttpMethod, R, B = unknown>(
    method: M,
    path: string,
    config: RequestConfig<M, B>
  ): Promise<ApiResponse<R>> {
    const fullPath = this.baseUrl + path;
    const init: RequestInit = { method };

    if ('body' in config && config.body) {
      init.body = JSON.stringify(config.body);
    }

    const url = new URL(fullPath);
    if (config.params) {
      Object.entries(config.params).forEach(([k, v]) =>
        url.searchParams.set(k, String(v))
      );
    }

    return request<R>(url.toString(), init);
  }
}

// 使用示例
const client = new ApiClient('https://api.example.com');

// GET请求:不需要body
const userRes = await client.call('GET', '/users/1', { method: 'GET' });

// 带类型参数的调用
interface CreateUserDTO { name: string; email: string }
interface UserDTO { id: number; name: string; email: string }

const created = await client.call<'POST', UserDTO, CreateUserDTO>(
  'POST', '/users',
  { method: 'POST', body: { name: 'test', email: 'test@test.com' } }
);
// created.data 类型为 UserDTO
// body 必须匹配 CreateUserDTO 类型,否则编译错误

映射类型生成API端点定义

利用映射类型和keyof操作符,可以从API定义对象自动生成类型安全的请求方法:

// API端点定义
interface UserApi {
  getUser: {
    method: 'GET';
    path: '/users/:id';
    params: { id: number };
    response: User;
  };
  createUser: {
    method: 'POST';
    path: '/users';
    body: { name: string; email: string };
    response: User;
  };
  updateUser: {
    method: 'PUT';
    path: '/users/:id';
    params: { id: number };
    body: { name?: string; email?: string };
    response: User;
  };
  deleteUser: {
    method: 'DELETE';
    path: '/users/:id';
    params: { id: number };
    response: void;
  };
}

// 从端点定义生成类型安全的API方法
type ApiMethods<T extends Record<string, any>> = {
  [K in keyof T]: T[K]['method'] extends 'GET' | 'DELETE'
    ? (params: T[K]['params']) => Promise<T[K]['response']>
    : (params: T[K]['params'], body: T[K]['body']) => Promise<T[K]['response']>
};

// 实现类型安全的API层
function createApi<T extends Record<string, any>>(
  client: ApiClient,
  endpoints: T
): ApiMethods<T> {
  const api = {} as ApiMethods<T>;

  for (const key of Object.keys(endpoints) as (keyof T)[]) {
    const ep = endpoints[key] as any;
    (api as any)[key] = async (...args: any[]) => {
      const path = buildPath(ep.path, args[0] || {});
      return client.call(ep.method, path, {
        method: ep.method,
        body: args[1],
      }).then(res => res.data);
    };
  }

  return api;
}

// 使用示例:完全类型安全
const userApi = createApi(client, {
  getUser: { method: 'GET', path: '/users/:id' },
  createUser: { method: 'POST', path: '/users' },
  updateUser: { method: 'PUT', path: '/users/:id' },
  deleteUser: { method: 'DELETE', path: '/users/:id' },
} satisfies UserApi);

// 编译时类型检查
const user = await userApi.getUser({ id: 1 });        // 正确
await userApi.createUser({ id: 0 }, { name: 'a', email: 'a@a.com' }); // 正确
// userApi.getUser({ id: '1' });  // 编译错误:id应为number

条件类型实现API响应数据提取

在API SDK中,不同接口返回的数据结构可能嵌套层级不同。条件类型可以精确提取嵌套类型:

// 深层属性提取
type DeepPick<T, P extends string> =
  P extends `${infer K}.${infer Rest}`
    ? K extends keyof T
      ? DeepPick<T[K], Rest>
      : never
    : P extends keyof T
    ? T[P]
    : never;

// 分页响应类型
interface PaginatedResponse<T> {
  code: number;
  data: {
    list: T[];
    total: number;
    page: number;
    pageSize: number;
  };
}

// 条件类型:判断是否为分页响应
type UnwrapResponse<T> =
  T extends PaginatedResponse<infer U>
    ? U[]
    : T extends ApiResponse<infer U>
    ? U
    : T;

// API方法返回类型自动推断
async function getList<T>(url: string): Promise<UnwrapResponse<PaginatedResponse<T>>> {
  const res = await fetch(url);
  const json: PaginatedResponse<T> = await res.json();
  return json.data.list;  // 返回 T[] 类型
}

interface Article {
  id: number;
  title: string;
}

const articles = await getList<Article>('/api/articles');
// articles 类型自动推断为 Article[]
articles.forEach(a => console.log(a.title));  // 类型安全

TypeScript泛型约束和条件类型在API SDK类型安全设计中发挥着关键作用。通过泛型参数传递请求和响应类型,结合条件类型实现HTTP方法级别的参数约束,再利用映射类型从接口定义自动生成类型安全的请求方法,可以在编译时捕获绝大部分API调用相关的类型错误。这种设计在前端工程化实践中减少了运行时调试成本,提升了代码可维护性。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/typescript-fan-xing-yue-shu-yu-tiao-jian-lei-xing-zai/

(0)
小编小编
上一篇 1小时前
下一篇 1小时前

相关推荐