泛型是一个强大的工具,可以帮助我们创建可复用的函数。在TypeScript中,我们可以声明变量和其他数据结构为特定类型,例如对象、布尔值或字符串类型。而通过使用泛型,我们可以处理传递给函数的多种类型的变量。
在这篇文章中,我们将学习如何通过泛型实现类型安全,同时不牺牲性能或效率。泛型允许我们在尖括号中定义一个类型参数,如。此外,它们还允许我们编写泛型类、方法和函数。
我们将深入探讨在TypeScript中使用泛型的方法,展示如何在函数、类和接口中使用它们。我们将会讨论如何传递默认泛型值、多个值以及条件值给泛型。最后,我们还会讨论如何为泛型添加约束。
一、TypeScript泛型(generics)是什么?
在TypeScript中,泛型是一种创建可复用组件或函数的方法,能够处理多种类型。它们允许我们在编译时构建数据结构,而不需要在编译时设置特定的类型。
泛型的作用是编写可复用的、类型安全的代码,变量的类型在编译时是已知的。这意味着我们可以动态定义参数或函数的类型,而这些类型会在编译之前声明。这在我们需要在应用程序中使用某些逻辑时非常有用;通过这些可复用的逻辑片段,我们可以创建接受和返回自己类型的函数。
我们可以使用泛型在编译时进行检查,消除类型转换,并在整个应用程序中实现其他泛型函数。没有泛型,我们的应用程序代码可能会在某个时候编译成功,但我们可能得不到预期的结果,这可能会将错误推到生产环境中。
通过使用泛型,我们可以参数化类型。这一强大的功能可以帮助我们创建可复用、通用和类型安全的类、接口和函数。
泛型的优势
类型安全:泛型确保在编译时进行类型检查,这样可以防止在运行时出现类型错误。
代码复用:使用泛型,我们可以编写一次代码,适用于多种数据类型,从而提高代码的复用性。
可读性和可维护性:泛型使代码更具可读性和可维护性,因为它们使我们能够明确地表达数据结构的意图和用途。
二、泛型示例
创建没有使用泛型的函数
让我们先来看一个简单的例子。下面是一个简单的函数,它将为对象数组添加新的属性。我们定义了一个接口MyObject,该接口包含一个id和一个pet属性:
interface MyObject {
id: number;
pet: string;
}
const myArray: MyObject[] = [
{ id: 1, pet: "dog" },
{ id: 2, pet: "cat" },
];
const newPropertyKey = "checkup";
const newPropertyValue: string = '2023-12-03';
const newPropertyAddition = myArray.map((obj) => ({
...obj,
[newPropertyKey]: newPropertyValue,
}));
console.log(newPropertyAddition);
在这个例子中,我们为数组中的每个对象添加了一个新的属性checkup。但假设我们有一个接受字符串的属性,并且我们希望添加一个接受数字的新属性,而不想重新编写另一个函数,这时泛型就派上用场了!
使用泛型创建函数
让我们来看一下如何使用泛型来解决这个问题。如果我们传递一个字符串数组给上面的函数,它将抛出错误:
'Type ‘number’ is not assignable to type of ‘string’
我们可以通过添加any到类型声明中来修复这个问题:
interface MyObject {
id: number;
pet: string;
}
const myArray: MyObject[] = [
{ id: 1, pet: "dog" },
{ id: 2, pet: "cat" },
];
const newPropertyKey = "checkup";
const newPropertyValue: any = 20231203;
const newPropertyAddition = myArray.map((obj) => ({
...obj,
[newPropertyKey]: newPropertyValue,
}));
console.log(newPropertyAddition);
然而,如果我们不定义特定的数据类型,那么使用TypeScript就没有意义了。让我们用泛型来重构这个函数:
type MyArray<T> = Array<T>;
type AddNewProperty<T> = {
[K in keyof T]: T[K];
} & { newProperty: string };
interface MyObject {
id: number;
pet: string;
}
const myArray: MyArray<MyObject> = [
{ id: 1, pet: "dog" },
{ id: 2, pet: "cat" },
];
const newPropertyAddition: MyArray<AddNewProperty<MyObject>> = myArray.map((obj) => ({
...obj,
newProperty: "New value",
}));
console.log(newPropertyAddition);
在这里,我们定义了一个名为的类型,使其更具通用性。它将持有由函数本身接收的数据类型。这意味着函数的类型现在是参数化的。
首先,我们定义一个表示对象数组的泛型类型MyArray,并创建另一个类型AddNewProperty,该类型向数组中的每个对象添加一个新属性。
为了提高清晰度,我们可以创建一个函数,该函数接受一个泛型作为参数并返回一个泛型:
function genericsPassed<T>(arg: T): [T] {
console.log(typeof(arg));
return [arg];
}
// 传递了一个数字
genericsPassed(3);
// 传递了一个日期对象
genericsPassed(new Date());
// 传递了一个正则表达式
genericsPassed(new RegExp("/([A-Z])\w+/g"));
使用泛型创建TypeScript类
让我们来看一个在类中使用泛型的例子:
class MyObject<T> {
id: number;
pet: string;
checkup: T;
constructor(id: number, pet: string, additionalProperty: T) {
this.id = id;
this.pet = pet;
this.checkup = additionalProperty;
}
}
const myArray: MyObject<string>[] = [
new MyObject(1, "cat", "false"),
new MyObject(2, "dog", "true"),
];
const newPropertyAddition: MyObject<number | boolean>[] = myArray.map((obj) => {
return new MyObject(obj.id, obj.pet, obj.id % 2 === 0);
});
console.log(newPropertyAddition);
在这个例子中,我们创建了一个名为MyObject的简单类,该类包含一个数组变量id、pet和checkup。我们还定义了一个泛型类MyObject,表示具有id、pet和类型为T的附加属性additionalProperty的对象。构造函数接受这些属性的值。
三、泛型接口的使用
泛型不仅限于函数和类,我们也可以在 TypeScript 中的接口内使用泛型。泛型接口使用类型参数作为占位符来表示未知的数据类型。当我们使用泛型接口时,可以用具体的类型填充这些占位符,从而定制结构以满足我们的需求。
示例:泛型接口的使用
基本示例
假设我们有一个函数 currentlyLoggedIn,它接收一个对象并返回包含 online 状态的扩展对象。以下是没有泛型的实现:
const currentlyLoggedIn = (obj: object): object => {
let isOnline = true;
return { ...obj, online: isOnline };
}
const user = currentlyLoggedIn({ name: 'Ben', email: 'ben@mail.com' });
const currentStatus = user.online; // Error: Property 'online' does not exist on type 'object'.
上面的代码会报错,因为 currentlyLoggedIn 函数不知道它接收到的对象类型。我们可以使用泛型来解决这个问题:
const currentlyLoggedIn = <T extends object>(obj: T) => {
let isOnline = true;
return { ...obj, online: isOnline };
}
const user = currentlyLoggedIn({ name: 'Benny barks', email: 'benny@mail.com' });
user.online = false; // No error
在这个例子中,我们用声明了一个泛型参数 T,函数可以处理任何对象类型,并且返回的对象包含 online 属性。
使用泛型接口
我们可以在接口中使用泛型来定义更复杂的数据结构。例如:
interface User<T> {
name: string;
email: string;
online: boolean;
skills: T;
}
const newUser: User<string[]> = {
name: "Benny barks",
email: "ben@mail.com",
online: false,
skills: ["chewing", "barking"],
};
const brandNewUser: User<number[]> = {
name: "Benny barks",
email: "benny@mail.com",
online: false,
skills: [2456234, 243534],
};
在这个例子中,是类型参数,可以在使用接口时替换为任何有效的 TypeScript 类型。
现实世界中的应用:泛型接口 ILogger
下面是一个现实世界中的例子,展示了如何使用泛型接口。我们创建了一个 ILogger 接口,定义了一个log方法,该方法接受消息和任意类型的数据(T):
interface ILogger<T> {
log(message: string, data: T): void;
}
ILogger 接口可以用于任何数据类型,使我们的代码更适应不同的场景,并确保记录的数据类型正确。
实现 ConsoleLogger 类
首先,我们创建一个实现 ILogger 接口的 ConsoleLogger 类:
class ConsoleLogger implements ILogger<any> {
log(message: string, data: any) {
console.log(`${message}:`, data);
}
}
const user = { name: "John Lee", age: 22 };
const consoleLogger = new ConsoleLogger();
consoleLogger.log("New user added", user);
我们可以使用 ConsoleLogger 打印消息和任何类型的数据到控制台。
实现 FileLogger 类
接下来,我们创建一个实现 ILogger 接口的 FileLogger 类,将消息记录到文件中:
class FileLogger implements ILogger<string> {
private filename: string;
constructor(filename: string) {
this.filename = filename;
}
log(message: string, data: string): void {
console.log(`Writing to file: ${this.filename}`);
// ... write logEntry to file ...
}
}
const fileLogger = new FileLogger("userlog.txt");
fileLogger.log("User information", JSON.stringify(user));
通过使用泛型接口 ILogger,我们可以实现一个通用的日志记录类,处理任何数据类型,使我们的代码更加灵活。
四、为泛型传递默认值
在 TypeScript 中,我们可以为泛型传递默认类型值。这在某些情况下非常有用,例如当我们不希望强制传递函数处理的数据类型时。通过设置默认类型,我们可以让泛型在没有明确指定类型时使用默认值。
示例:设置默认泛型类型
下面是一个示例,我们将泛型类型默认为 number:
function removeRandomArrayItem<T = number>(arr: Array<T>): Array<T> {
const randomIndex = Math.floor(Math.random() * arr.length);
return arr.splice(randomIndex, 1);
}
console.log(removeRandomArrayItem([45345, 3453, 356753, 3562345, 3567235]));
在这个代码片段中,我们使用了默认泛型类型 number 来实现 removeRandomArrayItem 函数。如果调用时不提供具体的类型参数,TypeScript 将使用默认类型 number。
为什么使用默认泛型类型
简化调用:默认泛型类型使函数调用更简单,不需要每次都指定类型参数。
提高灵活性:在某些情况下,用户可能不关心类型参数是什么,通过提供默认类型,我们可以让代码更灵活。减少冗余:在某些常见情况下,指定类型是多余的,通过默认值可以减少代码的冗余。
五 、传递多个泛型值
如果我们希望函数能够接受多个泛型参数,可以这样做:
function removeRandomAndMultiply<T = number, Y = number>(arr: Array<T>, multiply: Y): [T[], Y] {
const randomIndex = Math.floor(Math.random() * arr.length);
const multipliedVal = arr.splice(randomIndex, 1);
return [multipliedVal, multiply];
}
console.log(removeRandomAndMultiply([45345, 3453, 356753, 3562345, 3567235], 608));
在这个例子中,我们修改了之前的函数,引入了另一个泛型参数 Y。我们用字母 Y 表示,并将其默认类型设置为 number,因为它将用于乘以从数组中挑选的随机数。因为我们在处理数字,所以可以传递默认的泛型类型 number。
六、传递条件值给泛型
有时,我们可能希望传递符合某个条件的特定数量的值。我们可以通过定义一个带有条件泛型类型参数的类来实现这一点:
class MyNewClass<T extends { id: number }> {
petOwner: T[];
constructor(pets: T[]) {
this.petOwner = pets;
}
processPets<X>(callback: (pet: T) => X): X[] {
return this.petOwner.map(callback);
}
}
interface MyObject {
id: number;
pet: string;
}
const myArray: MyObject[] = [
{ id: 1, pet: "Dog" },
{ id: 2, pet: "Cat" },
];
const myClass = new MyNewClass(myArray);
const whichPet = myClass.processPets((item) => {
// 根据 item 属性添加条件逻辑
if (item.pet === 'Dog') {
return "You have a dog as a pet!";
} else {
return "You have a cat as a pet!";
}
});
console.log(whichPet);
在上述代码中,我们定义了一个类 MyNewClass<t,它扩展了包含 id 属性且类型为 number 的对象。该类有一个空数组属性 petOwner,类型为 T,用于存放项目。
MyNewClass 的 processPets 方法接受一个回调函数,该回调函数遍历每个项目并检查定义的条件。whichPet 的返回值将是一个基于回调函数中提供的条件的值数组。我们可以添加条件并定义逻辑,以根据需求和具体情况进行调整。
七 、为泛型添加约束
泛型允许我们处理作为参数传递的任何数据类型。然而,我们可以为泛型添加约束,以将其限制为特定类型。这样可以确保我们不会获取不存在的属性。
添加约束的示例 一个类型参数可以被声明为受限于另一个类型参数。这将帮助我们在对象上添加约束,确保我们不会获取不存在的属性:
function getObjProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
return obj[key];
}
let x = { name: "Benny barks", address: "New York", phone: 7245624534534, admin: false };
console.log(getObjProperty(x, "name")); // Benny barks
console.log(getObjProperty(x, "admin")); // false
console.log(getObjProperty(x, "loggedIn")); // Error: Property 'loggedIn' does not exist on type '{ name: string; address: string; phone: number; admin: boolean; }'
在上面的例子中,我们创建了一个函数getObjProperty,它接受两个参数:一个对象obj和一个键key。我们为第二个参数添加了一个约束Keyextendskeyof Type,确保传递的键必须是对象类型中的一个有效键。
为什么要添加约束
添加约束可以帮助我们在编译时捕获错误,而不是在运行时。这种方法提供了更高的类型安全性,防止了试图访问对象中不存在的属性。
八、动态数据类型的泛型实现
泛型允许我们在定义函数和数据结构时使用各种数据类型,并同时保持类型安全。当类型在运行时才确定时,我们可以使用泛型来定义函数;这些泛型类型将在运行时被具体的类型替换。通过传递泛型类型参数,我们可以处理包含多种数据类型的数组,反序列化JSON数据,或处理动态的HTTP响应数据。
使用泛型构建API客户端
假设我们正在构建一个与API交互的Web应用程序。我们需要创建一个能够处理不同API响应和各种数据结构的API客户端。我们可以定义一个API服务如下:
interface ApiResponse<T> {
data: T;
}
class ApiService {
private readonly baseUrl: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
}
public async get<T>(url: string): Promise<ApiResponse<T>> {
const response = await fetch(`${this.baseUrl}${url}`);
const data = await response.json() as T;
return { data };
}
}
在这里,我们定义了一个ApiResponse接口,它表示一个通用的API响应结构。该接口包含一个类型为T的data属性,还可以扩展其他属性(例如,状态、错误信息)。接着,我们创建了一个ApiService类,其中包括一个泛型函数,该函数接受一个URL路径并返回一个Promise<ApiResponse>。该函数从提供的URL获取数据,解析并断言JSON响应(data as T)。
使用泛型类型,ApiService类可以通过改变get函数中的类型参数T,在不同的API端点间重用。如下例所示,我们可以使用相同的apiClient来调用两个端点,分别获取客户端和产品:
interface Client {
id: number;
name: string;
email: string;
}
interface Product {
id: number;
name: string;
price: number;
}
const apiClient = new ApiService('https://api.example.com');
async function getClient(id: number): Promise<Client> {
const response = await apiClient.get<Client>(`/clients/${id}`);
return response.data;
}
async function getProducts(): Promise<Product[]> {
const response = await apiClient.get<Product[]>("/products");
return response.data;
}
// 示例调用
getClient(1).then(client => console.log(client));
getProducts().then(products => console.log(products));
在这个例子中,getClient函数和getProducts函数使用相同的apiClient来调用不同的端点,并获取不同类型的数据。通过使用泛型,我们能够在编译时确保类型安全,并在运行时根据实际需求处理不同的数据类型。
通过泛型,我们可以编写更加灵活和可复用的代码,特别是在处理动态数据类型时。泛型在API客户端的实现中尤为有用,它允许我们在不同的API端点间共享代码,同时保持类型安全。掌握这些技巧,可以帮助我们构建更加健壮和高效的应用程序。
九、关于泛型的一些注意事项
TypeScript 的泛型是一种强大的工具,但在大型代码库中使用它们时,需要了解一些最佳实践。
1. 使用描述性名称
在定义泛型接口或函数时,使用清晰和描述性的类型参数名称。这样可以更准确地反映预期的数据类型,使代码更易读和可维护。
例如,我们定义一个doubleValue函数。这个泛型函数表达了函数的预期类型和意图,使代码更易于理解和维护:
function doubleValue<T extends number>(value: T): T {
return value * 2;
}
2. 必要时应用约束
使用类型约束(extends关键字)来限制可以与泛型一起使用的类型,确保只接受兼容的类型。
在下面的示例中,定义了一个泛型接口并将其应用为参数约束,因此findById函数只接受实现特定接口的对象:
interface Identifiable<T> {
id: T;
}
function findById<T, U extends Identifiable<T>>(collection: U[], id: T) {
return collection.find(item => item.id === id);
}
3. 利用实用类型
TypeScript 提供了一些实用类型(如Partial、Readonly和Pick<T, K>),以便于常见的数据操作。这些类型可以增强代码的可读性和可维护性。
例如,Partial 创建一个可选属性的类型:
interface User {
id: number;
name: string;
email?: string;
}
// Partial 创建一个可选属性的类型
type UserPartial = Partial<User>;
const userData: UserPartial = { name: "Alice" }; // 只给出部分属性
4. 使用泛型默认值
在某些情况下,可以为泛型参数提供默认值,以减少使用泛型时的复杂性。
interface ApiResponse<T = any> {
data: T;
status: number;
message: string;
}
// 默认情况下,T 是 `any`
const response: ApiResponse = {
data: { name: "Alice" },
status: 200,
message: "Success"
};
5. 避免过度泛型化
不要过度使用泛型。虽然泛型很强大,但不必要的泛型化会使代码复杂化并难以理解。只在需要的地方使用泛型。
6. 文档化和注释
在代码中使用泛型时,确保有良好的文档和注释,解释泛型参数的用途和限制。这有助于其他开发人员理解和使用你的代码。
十、 TypeScript 泛型常见问题
在使用 TypeScript 泛型时,我们经常会遇到类似“type is not generic”的问题。解决这些问题需要系统的方法和对泛型在 TypeScript 中工作原理的理解。以下是一些常见问题及其解决策略。
常见问题及解决策略
1. “Type is not generic” / “Generic typerequirestypeargument”
这个错误通常发生在使用泛型类型而没有提供必要的类型参数时,或者在使用非泛型类型时使用了类型参数。解决方法是指定数组应该包含的元素类型。例如,在下面的代码片段中,修正的方法是添加类型参数,如 const foo:Array= [1, 2, 3];:
interface User {
id: number;
}
// 尝试将 User 用作泛型参数
const user: User<number> = {}; // Type is not generic
const foo: Array = [1, 2, 3]; // Generic type 'Array' requires 1 type argument(s).
解决方法
确保你在使用泛型类型时提供了必要的类型参数:
interface User {
id: number;
}
// 正确使用 User 类型
const user: User = { id: 1 };
const foo: Array<number> = [1, 2, 3]; // 正确添加类型参数
2. “Cannot Find Name 'T'”
这个错误通常发生在使用未声明或不在作用域内的类型参数(T)时。要解决此问题,请正确声明类型参数或检查其使用中的拼写错误:
// 尝试在未声明类型参数的情况下使用 T 作为泛型类型参数
function getValue(value: T): T { // Cannot find name 'T'.
return value;
}
// 通过声明 T 作为泛型类型参数修复错误
function getValue<T>(value: T): T {
return value;
}
结束
在这篇文章中,我们深入探讨了 TypeScript 泛型的强大功能及其最佳实践。通过具体的示例和详细的解释,我们展示了如何利用泛型创建灵活、可复用且类型安全的代码。泛型不仅能帮助我们减少运行时错误的风险,还能显著提高代码的可维护性和可读性。
希望这篇文章能帮助你更好地理解和应用 TypeScript 中的泛型。如果你在实践中遇到任何问题,或者有任何想法和建议,欢迎在评论区与我们交流讨论!如果你觉得这篇文章对你有所帮助,不妨分享给你的朋友,让更多人受益。
感谢你的阅读,我们下次再见!