泛型是 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 内置的 Partial、Pick、Record 等都是典型的高阶泛型。我们也可以自定义高阶泛型来解决特定问题。
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 泛型的进阶用法:
- 泛型约束:通过
extends、keyof和交叉类型,让类型参数具备必要的结构,从而在函数体内安全地访问属性和方法。 - 默认类型参数:为泛型提供合理的默认值,降低调用方的使用成本,同时保留完整的类型控制能力。
- 高阶泛型:利用条件类型、映射类型和递归类型,构建可复用的类型工具,实现复杂的类型转换和推导。
掌握这些技术后,你将能够设计出既灵活又类型安全的 API,在大型项目中显著提升代码的可维护性和开发体验。建议在实际项目中逐步应用这些模式,从简单的泛型约束开始,逐步过渡到高阶泛型的组合使用。
未经允许不得转载:任鹏个人博客 » TypeScript 泛型进阶:高阶泛型、泛型约束与默认类型参数的实战应用


朋友圈点赞图在线生成源码