泛型(Generics)是 TypeScript 的一个强大功能,它允许我们在定义函数、类或接口时,使用类型参数来进行类型的约束。通过泛型,我们可以在保持类型安全的同时,实现代码的复用。
使用泛型的最大优势是能够让你编写可重用的代码,同时确保类型安全。假设你在写一个函数,它能处理多种类型的数据,但你不确定到底是哪种类型。使用泛型可以帮助你在不丧失类型检查的情况下,处理不同的数据类型。
泛型的基本用法
泛型函数
泛型函数允许你在函数定义时指定类型参数。比如:
function identity<T>(arg: T): T {
return arg;
}
const num = identity(42); // num 的类型是 number
const str = identity('Moment'); // str 的类型是 string
在上面的代码中:
T
是一个类型参数,它并不代表任何特定的类型。identity
函数返回的类型与参数的类型相同。这里通过泛型保证了类型安全。
泛型数组
你可以使用泛型来定义数组的类型:
function logArray<T>(arr: T[]): void {
arr.forEach((item) => console.log(item));
}
logArray([1, 2, 3]); // 输出数字
logArray(['a', 'b', 'c']); // 输出字符串
在这里,T[]
表示 arr
是一个包含类型 T
的数组,T
可以是任何类型。
泛型接口
接口也可以使用泛型,通常用于定义那些在类型不确定时仍然能有效工作的接口:
interface Box<T> {
value: T;
}
const numberBox: Box<number> = { value: 42 };
const stringBox: Box<string> = { value: 'Hello' };
在这个例子中,Box
是一个泛型接口,它定义了一个 value
属性,这个属性的类型由 T
来决定。
泛型类
你也可以在类中使用泛型:
class Container<T> {
value: T;
constructor(value: T) {
this.value = value;
}
getValue(): T {
return this.value;
}
}
const numContainer = new Container(42);
const strContainer = new Container('Moment');
console.log(numContainer.getValue()); // 42
console.log(strContainer.getValue()); // "Moment"
这里,Container
是一个泛型类,可以接收不同的类型。
泛型约束
有时你可能希望泛型类型参数能够满足某种条件,比如它必须是一个特定类型的子类。可以使用泛型约束来实现:
function logLength<T extends { length: number }>(item: T): void {
console.log(item.length);
}
logLength([1, 2, 3]); // 输出 3
logLength('Hello'); // 输出 5
// 下面的代码会报错,因为 number 类型没有 `length` 属性
// logLength(123);
T extends { length: number }
这个约束表示,T
必须是一个具有 length
属性的类型。
如下图所示:
你也可以为泛型指定默认类型:
function wrap<T = string>(value: T): T {
return value;
}
const wrapped = wrap(42); // 自动推断为 number 类型
const wrappedString = wrap('Hello'); // 使用默认的 string 类型
在这个例子中,如果没有显式指定 T
的类型,T
会默认为 string
。
多个泛型类型参数
一个函数或接口可以使用多个泛型参数:
function combine<T, U>(a: T, b: U): [T, U] {
return [a, b];
}
const result = combine(42, 'Hello'); // result 是 [number, string]
在上面的例子中,combine
函数使用了两个类型参数:T
和 U
。
项目场景:API 请求库
假设你在开发一个 API 请求库,功能是发送 HTTP 请求并处理响应数据。因为 API 返回的数据格式会根据不同的接口而有所不同,因此你希望能够保证每次请求都能按照正确的类型来处理响应数据。
如下代码所示:
async function apiRequest<T>(url: string, method: string = 'GET', body: any = null): Promise<T> {
const response = await fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json',
},
body: body ? JSON.stringify(body) : null,
});
if (!response.ok) {
throw new Error(`Request failed with status ${response.status}`);
}
// 假设 API 返回的 JSON 数据符合 T 类型
return response.json();
}
接下来我们使用泛型来发起请求并确保类型安全,如下代码所示:
interface User {
id: number;
name: string;
}
interface Product {
id: number;
title: string;
price: number;
}
// 请求用户数据
async function fetchUserData(userId: number): Promise<User> {
const url = `https://api.example.com/users/${userId}`;
return apiRequest<User>(url); // 传入 User 类型作为泛型参数
}
// 请求商品数据
async function fetchProductData(productId: number): Promise<Product> {
const url = `https://api.example.com/products/${productId}`;
return apiRequest<Product>(url); // 传入 Product 类型作为泛型参数
}
async function main() {
try {
const user = await fetchUserData(1);
console.log(user.id, user.name); // 类型安全,user 是 User 类型
const product = await fetchProductData(101);
console.log(product.id, product.title, product.price); // 类型安全,product 是 Product 类型
} catch (error) {
console.error(error);
}
}
main();
在上面的代码中,apiRequest<T>
是一个泛型函数,用于发送 HTTP 请求并根据传入的泛型类型(如 User
或 Product
)确保返回的数据符合预期结构。通过泛型,TypeScript 可以在编译时检查响应数据的类型,提供类型安全,防止数据结构不匹配的问题。
如下所示:
总结
TypeScript 的泛型(Generics)允许我们在函数、类和接口中使用类型参数,从而实现代码的复用和类型安全。通过泛型,我们可以处理多种类型的数据,并确保返回值符合预期的结构。它支持类型约束、多个类型参数和默认类型,使得代码更加灵活和可维护。泛型在实际项目中尤其适用于处理动态类型的数据,如 API 请求库中确保响应数据的类型安全。