TypeScript 泛型进阶:高阶泛型、泛型约束与默认类型参数的实战应用

泛型是 TypeScript 类型系统中最强大的特性之一。很多开发者对泛型的理解停留在 Array<T> 或简单的 function identity<T>(arg: T): T 层面,但在实际工程中,泛型的能力远不止于此。本文将深入探讨高阶泛型、泛型约束与默认类型参数三大进阶主题,并通过贴近实战的示例,帮助你写出更具表现力和类型安全性的代码。

一、泛型约束:让类型参数“有据可依”

默认情况下,泛型参数可以是任意类型。但很多时候,我们需要对类型参数施加一定的限制,确保它具备某些属性或结构。这就是泛型约束的作用。

1.1 使用 extends 进行基础约束

function getLength<T extends { length: number }>(item: T): number {
  return item.length;
}

getLength("hello");        // ✅ string 有 length
getLength([1, 2, 3]);      // ✅ array 有 length
getLength({ length: 10 }); // ✅ 对象有 length
// getLength(123);         // ❌ number 没有 length

通过 T extends { length: number },我们告诉编译器:T 必须拥有 length 属性。这样在函数体内就可以安全地访问 item.length

1.2 使用 keyof 约束键名

这是泛型约束中最常见的模式之一:

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

const user = { name: "Alice", age: 30 };
const name = getProperty(user, "name"); // string
const age = getProperty(user, "age");   // number
// getProperty(user, "email");          // ❌ 编译错误

K extends keyof T 确保了传入的 key 一定是对象 T 的合法属性名,返回值类型也能被精确推断为 T[K]

1.3 多重约束与交叉类型

有时需要同时满足多个约束:

interface Serializable {
  serialize(): string;
}

interface Loggable {
  log(): void;
}

function process<T extends Serializable & Loggable>(item: T): string {
  item.log();
  return item.serialize();
}

二、默认类型参数:提升 API 的易用性

默认类型参数允许你为泛型指定一个“后备类型”,当调用方没有显式传入类型参数时自动生效。这在设计库或公共 API 时尤为有用。

2.1 基础用法

interface ApiResponse<T = unknown> {
  data: T;
  status: number;
  message: string;
}

// 不传类型参数时,data 为 unknown
const res1: ApiResponse = { data: null, status: 200, message: "ok" };

// 传入类型参数时,data 为具体类型
const res2: ApiResponse<User[]> = {
  data: [{ name: "Alice", age: 30 }],
  status: 200,
  message: "ok",
};

2.2 默认参数与约束结合

默认类型参数也可以与约束一起使用:

type EventMap = {
  click: { x: number; y: number };
  keydown: { key: string };
};

function addListener<
  T extends keyof EventMap = keyof EventMap
>(event: T, handler: (payload: EventMap[T]) => void): void {
  // ...
}

addListener("click", (payload) => {
  console.log(payload.x, payload.y); // 类型安全
});

2.3 实战:可配置的 HTTP 客户端

interface RequestConfig<TBody = unknown, TQuery = Record<string, string>> {
  url: string;
  method: "GET" | "POST" | "PUT" | "DELETE";
  body?: TBody;
  query?: TQuery;
}

function request<TResponse, TBody = unknown, TQuery = Record<string, string>>(
  config: RequestConfig<TBody, TQuery>
): Promise<TResponse> {
  // 实现略
  return Promise.resolve({} as TResponse);
}

通过默认类型参数,调用方在简单场景下无需指定所有泛型,而在复杂场景下又能获得完整的类型控制。

三、高阶泛型:泛型作为类型操作的工具

高阶泛型指的是“接受泛型参数并返回新类型”的类型函数。TypeScript 内置的 PartialPickRecord 等都是典型的高阶泛型。我们也可以自定义高阶泛型来解决特定问题。

3.1 条件类型与泛型的结合

type IsArray<T> = T extends any[] ? true : false;

type A = IsArray<string[]>; // true
type B = IsArray<number>;   // false

3.2 映射类型:批量转换属性

type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

interface User {
  name: string;
  age: number;
}

type NullableUser = Nullable<User>;
// { name: string | null; age: number | null }

3.3 实战:构建类型安全的表单验证器

假设我们要创建一个表单验证系统,每个字段有对应的验证规则,并且验证结果需要精确到字段级别:

type Validator<T> = (value: T) => string | null;

type FormSchema<T> = {
  [K in keyof T]: Validator<T[K]>;
};

type ValidationResult<T> = {
  [K in keyof T]?: string;
};

function validate<T extends Record<string, unknown>>(
  data: T,
  schema: FormSchema<T>
): ValidationResult<T> {
  const errors: ValidationResult<T> = {};
  for (const key in schema) {
    const error = schema[key](data[key]);
    if (error) {
      errors[key] = error;
    }
  }
  return errors;
}

// 使用
interface LoginForm {
  username: string;
  password: string;
}

const result = validate<LoginForm>(
  { username: "", password: "123" },
  {
    username: (v) => (v.length === 0 ? "用户名不能为空" : null),
    password: (v) => (v.length < 6 ? "密码至少6位" : null),
  }
);
// result: { username?: string; password?: string }

这个例子综合运用了泛型约束、映射类型和条件推断,实现了字段级别的类型安全。

3.4 递归泛型:处理深层嵌套结构

type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

interface Config {
  server: {
    host: string;
    port: number;
    ssl: { enabled: boolean; cert: string };
  };
  debug: boolean;
}

type PartialConfig = DeepPartial<Config>;
// 所有层级属性都变为可选

四、综合实战:类型安全的事件总线

将上述技术综合起来,实现一个类型安全的事件总线:

type EventHandler<T> = (payload: T) => void;

class EventBus<Events extends Record<string, unknown>> {
  private handlers: {
    [K in keyof Events]?: Array<EventHandler<Events[K]>>;
  } = {};

  on<K extends keyof Events>(event: K, handler: EventHandler<Events[K]>): void {
    if (!this.handlers[event]) {
      this.handlers[event] = [];
    }
    this.handlers[event]!.push(handler);
  }

  emit<K extends keyof Events>(event: K, payload: Events[K]): void {
    this.handlers[event]?.forEach((h) => h(payload));
  }
}

// 使用
interface AppEvents {
  login: { userId: string; timestamp: number };
  logout: { userId: string };
  error: { code: number; message: string };
}

const bus = new EventBus<AppEvents>();

bus.on("login", (payload) => {
  console.log(payload.userId, payload.timestamp); // 类型安全
});

bus.emit("login", { userId: "u1", timestamp: Date.now() });
// bus.emit("login", { userId: "u1" }); // ❌ 缺少 timestamp

五、总结

本文从三个维度深入探讨了 TypeScript 泛型的进阶用法:

  • 泛型约束:通过 extendskeyof 和交叉类型,让类型参数具备必要的结构,从而在函数体内安全地访问属性和方法。
  • 默认类型参数:为泛型提供合理的默认值,降低调用方的使用成本,同时保留完整的类型控制能力。
  • 高阶泛型:利用条件类型、映射类型和递归类型,构建可复用的类型工具,实现复杂的类型转换和推导。

掌握这些技术后,你将能够设计出既灵活又类型安全的 API,在大型项目中显著提升代码的可维护性和开发体验。建议在实际项目中逐步应用这些模式,从简单的泛型约束开始,逐步过渡到高阶泛型的组合使用。

未经允许不得转载:任鹏个人博客 » TypeScript 泛型进阶:高阶泛型、泛型约束与默认类型参数的实战应用

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏