Featured image of post TypeScript 通用约束:构建类型安全的 APIFeatured image of post TypeScript 通用约束:构建类型安全的 API

TypeScript 通用约束:构建类型安全的 API

了解如何使用 TypeScript 泛型约束(包括扩展、条件类型、映射类型和推断模式)构建类型安全的 API。

TypeScript 泛型允许您编写可重用、类型安全的代码,但如果没有约束,它们就过于宽松了。 extends 关键字将类型参数限制为具有特定属性的形状,为您提供灵活性和安全性。

扩展约束

通用约束的基础是 extends 关键字。它确保类型参数满足所需的结构:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

这里 K extends keyof T 保证 keyobj 的有效属性。尝试传递无效密钥会触发编译时错误。常见模式包括对接口 (T extends { id: string })、联合成员或原始类型的约束。


条件类型

条件类型使用 extends 在编译时检查类型关系并选择类型:

type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>;      // false

这使得类型级函数能够根据结构转换输入。一个实际的例子是解包 Promise 类型:

type Unwrap<T> = T extends Promise<infer R> ? R : T;
type Result = Unwrap<Promise<number>>; // number

条件类型分布在联合上。 IsString<string | number> 的计算结果为 boolean (true | false),独立地将条件应用于每个联合成员。


使用 keyof 的映射类型

映射类型会转换现有类型的每个属性。与 keyof 结合使用,它们可以实现强大的对象转换:

type Readonly<T> = { readonly [P in keyof T]: T[P] };
type Optional<T> = { [P in keyof T]?: T[P] };

TypeScript 提供了几种基于此模式构建的内置映射类型:

类型描述使用示例
占位符_0所有属性可选PATCH 请求正文
占位符_0所需的所有属性表格提交
占位符_0所有属性均为只读不可变的配置
占位符_0选择键的子集从实体查看模型
占位符_0词典类型查找表

从 TypeScript 4.1 开始,映射类型中的 as 子句支持使用模板文字类型进行键重新映射:

type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

推断关键字

条件类型中的 infer 关键字从其他类型中提取类型。它为多种内置实用程序类型提供支持:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Parameters<T> = T extends (...args: infer P) => any ? P : never;

实际应用包括提取组件属性类型、从第三方库派生函数签名以及解包嵌套泛型类型。 infer 位置决定提取的内容 — 返回类型、参数元组或构造函数参数。


有约束的工厂功能

通用约束对于创建符合特定形状的对象的工厂函数至关重要:

function createInstance<T, U extends T>(
  ctor: new (...args: any[]) => T,
  data: U
): T {
  return Object.assign(new ctor(), data);
}

该模式广泛应用于依赖注入容器、ORM 实体工厂和 API 响应转换器。约束 U extends T 确保数据至少包含 T 的所有属性,同时允许附加字段。


构建类型安全的 API 客户端

类型安全的 API 客户端演示了所有这些协同工作的概念:

type ApiResponse<T> =
  | { data: T; error: null }
  | { data: null; error: string };

type Endpoints = {
  "/users": User;
  "/posts": Post;
};

async function fetchApi<E extends keyof Endpoints>(
  endpoint: E
): Promise<ApiResponse<Endpoints[E]>> {
  const res = await fetch(endpoint);
  return res.json();
}

此处,条件类型从端点定义推断响应形状,extends 验证路由路径,查找类型 Endpoints[E] 检索正确的响应类型。


性能考虑因素

深度嵌套的泛型类型会减慢编译速度。通过避免过度递归的条件类型、使用接口合并而不是复杂的映射类型以及使用辅助别名缓存计算类型来缓解这一问题。使用 tsc --generateTrace 来识别瓶颈。


掌握 extends、条件类型、映射类型和 infer 可将 TypeScript 转变为强大的类型编程语言。这些模式可以防止所有类别的运行时错误,并通过编译时保证提供卓越的开发人员体验。