一文搞懂TypeScript泛型,让你的组件复用性大幅提升

泛型是一个强大的工具,可以帮助我们创建可复用的函数。在TypeScript中,我们可以声明变量和其他数据结构为特定类型,例如对象、布尔值或字符串类型。而通过使用泛型,我们可以处理传递给函数的多种类型的变量。

在这篇文章中,我们将学习如何通过泛型实现类型安全,同时不牺牲性能或效率。泛型允许我们在尖括号中定义一个类型参数,如。此外,它们还允许我们编写泛型类、方法和函数。

我们将深入探讨在TypeScript中使用泛型的方法,展示如何在函数、类和接口中使用它们。我们将会讨论如何传递默认泛型值、多个值以及条件值给泛型。最后,我们还会讨论如何为泛型添加约束。

一、TypeScript泛型是什么?

在TypeScript中,泛型是一种创建可复用组件或函数的方法,能够处理多种类型。它们允许我们在编译时构建数据结构,而不需要在编译时设置特定的类型。

泛型的作用是编写可复用的、类型安全的代码,变量的类型在编译时是已知的。这意味着我们可以动态定义参数或函数的类型,而这些类型会在编译之前声明。这在我们需要在应用程序中使用某些逻辑时非常有用;通过这些可复用的逻辑片段,我们可以创建接受和返回自己类型的函数。

我们可以使用泛型在编译时进行检查,消除类型转换,并在整个应用程序中实现其他泛型函数。没有泛型,我们的应用程序代码可能会在某个时候编译成功,但我们可能得不到预期的结果,这可能会将错误推到生产环境中。

通过使用泛型,我们可以参数化类型。这一强大的功能可以帮助我们创建可复用、通用和类型安全的类、接口和函数。

泛型的优势

  • 类型安全:泛型确保在编译时进行类型检查,这样可以防止在运行时出现类型错误。
  • 代码复用:使用泛型,我们可以编写一次代码,适用于多种数据类型,从而提高代码的复用性。
  • 可读性和可维护性:泛型使代码更具可读性和可维护性,因为它们使我们能够明确地表达数据结构的意图和用途。

二、TypeScript泛型实战示例

创建没有使用泛型的函数

让我们先来看一个简单的例子。下面是一个简单的函数,它将为对象数组添加新的属性。我们定义了一个接口MyObject,该接口包含一个id和一个pet属性:

ini 复制代码
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到类型声明中来修复这个问题:

ini 复制代码
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就没有意义了。让我们用泛型来重构这个函数:

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,该类型向数组中的每个对象添加一个新属性。

为了提高清晰度,我们可以创建一个函数,该函数接受一个泛型作为参数并返回一个泛型:

scss 复制代码
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类

让我们来看一个在类中使用泛型的例子:

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 状态的扩展对象。以下是没有泛型的实现:

ini 复制代码
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 函数不知道它接收到的对象类型。我们可以使用泛型来解决这个问题:

ini 复制代码
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 属性。

使用泛型接口

我们可以在接口中使用泛型来定义更复杂的数据结构。例如:

typescript 复制代码
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):

php 复制代码
interface ILogger<T> {
    log(message: string, data: T): void;
}

ILogger 接口可以用于任何数据类型,使我们的代码更适应不同的场景,并确保记录的数据类型正确。

实现 ConsoleLogger 类

首先,我们创建一个实现 ILogger 接口的 ConsoleLogger 类:

typescript 复制代码
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 类,将消息记录到文件中:

typescript 复制代码
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:

typescript 复制代码
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。

为什么使用默认泛型类型

  • 简化调用:默认泛型类型使函数调用更简单,不需要每次都指定类型参数。
  • 提高灵活性:在某些情况下,用户可能不关心类型参数是什么,通过提供默认类型,我们可以让代码更灵活。 减少冗余:在某些常见情况下,指定类型是多余的,通过默认值可以减少代码的冗余。

五 、传递多个泛型值

如果我们希望函数能够接受多个泛型参数,可以这样做:

ini 复制代码
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。

六、传递条件值给泛型

有时,我们可能希望传递符合某个条件的特定数量的值。我们可以通过定义一个带有条件泛型类型参数的类来实现这一点:

typescript 复制代码
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 extends { id: number }>,该类接受一个泛型类型参数 ,它扩展了包含 id 属性且类型为 number 的对象。该类有一个空数组属性 petOwner,类型为 T,用于存放项目。

MyNewClass 的 processPets 方法接受一个回调函数,该回调函数遍历每个项目并检查定义的条件。whichPet 的返回值将是一个基于回调函数中提供的条件的值数组。我们可以添加条件并定义逻辑,以根据需求和具体情况进行调整。

七 、为泛型添加约束

泛型允许我们处理作为参数传递的任何数据类型。然而,我们可以为泛型添加约束,以将其限制为特定类型。这样可以确保我们不会获取不存在的属性。

添加约束的示例 一个类型参数可以被声明为受限于另一个类型参数。这将帮助我们在对象上添加约束,确保我们不会获取不存在的属性:

vbnet 复制代码
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。我们为第二个参数添加了一个约束Key extends keyof Type,确保传递的键必须是对象类型中的一个有效键。

为什么要添加约束

添加约束可以帮助我们在编译时捕获错误,而不是在运行时。这种方法提供了更高的类型安全性,防止了试图访问对象中不存在的属性。

八、动态数据类型的泛型实现

泛型允许我们在定义函数和数据结构时使用各种数据类型,并同时保持类型安全。当类型在运行时才确定时,我们可以使用泛型来定义函数;这些泛型类型将在运行时被具体的类型替换。通过传递泛型类型参数,我们可以处理包含多种数据类型的数组,反序列化JSON数据,或处理动态的HTTP响应数据。

使用泛型构建API客户端

假设我们正在构建一个与API交互的Web应用程序。我们需要创建一个能够处理不同API响应和各种数据结构的API客户端。我们可以定义一个API服务如下:

typescript 复制代码
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。该函数从提供的URL获取数据,解析并断言JSON响应(data as T)。

使用泛型类型,ApiService类可以通过改变get函数中的类型参数T,在不同的API端点间重用。如下例所示,我们可以使用相同的apiClient来调用两个端点,分别获取客户端和产品:

typescript 复制代码
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函数。这个泛型函数表达了函数的预期类型和意图,使代码更易于理解和维护:

r 复制代码
function doubleValue<T extends number>(value: T): T {
    return value * 2;
}

2. 必要时应用约束

使用类型约束(extends关键字)来限制可以与泛型一起使用的类型,确保只接受兼容的类型。

在下面的示例中,定义了一个泛型接口并将其应用为参数约束,因此findById函数只接受实现特定接口的对象:

r 复制代码
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 创建一个可选属性的类型:

ini 复制代码
interface User {
    id: number;
    name: string;
    email?: string;
}

// Partial 创建一个可选属性的类型
type UserPartial = Partial<User>; 
const userData: UserPartial = { name: "Alice" }; // 只给出部分属性

4. 使用泛型默认值

在某些情况下,可以为泛型参数提供默认值,以减少使用泛型时的复杂性。

css 复制代码
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 type requires type argument"

这个错误通常发生在使用泛型类型而没有提供必要的类型参数时,或者在使用非泛型类型时使用了类型参数。解决方法是指定数组应该包含的元素类型。例如,在下面的代码片段中,修正的方法是添加类型参数,如 const foo: Array = [1, 2, 3];:

typescript 复制代码
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).

解决方法

确保你在使用泛型类型时提供了必要的类型参数:

typescript 复制代码
interface User {
    id: number;
}

// 正确使用 User 类型
const user: User = { id: 1 };

const foo: Array<number> = [1, 2, 3]; // 正确添加类型参数

2. "Cannot Find Name 'T'"

这个错误通常发生在使用未声明或不在作用域内的类型参数(T)时。要解决此问题,请正确声明类型参数或检查其使用中的拼写错误:

r 复制代码
// 尝试在未声明类型参数的情况下使用 T 作为泛型类型参数
function getValue(value: T): T { // Cannot find name 'T'.
    return value;
}

// 通过声明 T 作为泛型类型参数修复错误
function getValue<T>(value: T): T {
    return value;
}

结束

在这篇文章中,我们深入探讨了 TypeScript 泛型的强大功能及其最佳实践。通过具体的示例和详细的解释,我们展示了如何利用泛型创建灵活、可复用且类型安全的代码。泛型不仅能帮助我们减少运行时错误的风险,还能显著提高代码的可维护性和可读性。

希望这篇文章能帮助你更好地理解和应用 TypeScript 中的泛型。如果你觉得本文对你有所帮助,请分享给你的朋友,并在评论区留下你的看法和问题。关注我的公众号「前端达人」,获取更多关于 TypeScript 和其他前沿技术的精彩内容。让我们一起写出更优雅、更健壮的代码!

相关推荐
崔庆才丨静觅4 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60615 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了5 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅5 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅5 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅6 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment6 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅6 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊6 小时前
jwt介绍
前端
爱敲代码的小鱼6 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax