TypeScript 类、泛型的使用实践记录 | 豆包MarsCode AI刷题

58 阅读4分钟

TypeScript 中类与泛型的使用实践

TypeScript 是一个非常强大的工具,它通过静态类型检查提升了代码的安全性和可读性。尤其是 泛型,在开发中非常常见且实用。泛型(Generics)是 TypeScript 中的一项核心特性,它允许我们编写灵活、可复用的代码,同时还能保持强类型约束。本文将结合实际案例,探讨类与泛型在 TypeScript 中的使用,以及如何通过类型约束提升代码质量。


1. 什么是泛型?

泛型的本质是为类型参数化提供支持。它允许我们在定义函数、接口或类时使用占位符(比如 T),具体的类型在调用时由用户指定。

一个简单的例子

普通函数:

function identity(value: any): any {
  return value;
}

带泛型的函数:

function identity<T>(value: T): T {
  return value;
}

// 使用时
const result1 = identity<string>("Hello"); // T 被指定为 string
const result2 = identity<number>(123);     // T 被指定为 number

泛型相比于 any 的优势在于它保留了参数与返回值之间的类型关系,让代码更加安全。例如,在上述代码中,result1 会自动推断为 string,避免了类型错误。


2. 泛型在类中的使用

在类中,泛型可以帮助我们实现更加通用的逻辑。比如,我们可以创建一个支持任意类型的 数据存储类

案例:数据存储类

class DataStorage<T> {
  private storage: T[] = [];

  addItem(item: T): void {
    this.storage.push(item);
  }

  removeItem(item: T): void {
    this.storage = this.storage.filter(storedItem => storedItem !== item);
  }

  getItems(): T[] {
    return [...this.storage];
  }
}

// 使用
const stringStorage = new DataStorage<string>();
stringStorage.addItem("Apple");
stringStorage.addItem("Banana");
stringStorage.removeItem("Apple");
console.log(stringStorage.getItems()); // 输出: ['Banana']

const numberStorage = new DataStorage<number>();
numberStorage.addItem(10);
numberStorage.addItem(20);
console.log(numberStorage.getItems()); // 输出: [10, 20]

分析

  • 灵活性:通过泛型 TDataStorage 支持任意类型,而不需要为每种类型单独定义存储逻辑。
  • 类型安全:添加或移除的元素必须是同一种类型,编译时即可发现错误。

3. 泛型约束

有时,我们不希望泛型能够接受任何类型,而是希望它符合某些条件(比如拥有某些属性或方法)。这时候可以使用泛型约束。

案例:对象类型的约束

假设我们需要一个函数,打印传入对象的某个属性值。我们可以通过约束泛型来确保传入的对象具有特定的属性。

interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(item: T): void {
  console.log(`Length is: ${item.length}`);
}

// 使用
logLength({ length: 10 });         // 输出: Length is: 10
logLength("Hello World");          // 输出: Length is: 11
logLength([1, 2, 3, 4]);           // 输出: Length is: 4
// logLength(123);  // 报错: number 类型没有 length 属性

分析

  • T extends HasLength 表示泛型 T 必须拥有 length 属性。
  • 这样可以避免传入不合法的类型(比如数字 123),增强了代码的健壮性。

4. 泛型与接口的结合

泛型可以与接口结合使用,定义更加灵活的数据结构。

案例:响应数据的通用接口

在处理后端接口时,不同的 API 返回的数据结构可能不同,但通常有类似的封装,比如 statusdata 字段。我们可以使用泛型创建一个通用的响应接口:

interface ApiResponse<T> {
  status: string;
  data: T;
}

// 使用
const userResponse: ApiResponse<{ name: string; age: number }> = {
  status: "success",
  data: { name: "Alice", age: 25 },
};

const productResponse: ApiResponse<string[]> = {
  status: "success",
  data: ["Apple", "Banana", "Cherry"],
};

分析

  • ApiResponse<T> 的泛型 T 可以代表任意类型的数据(如对象、数组等)。
  • 在调用接口时,我们能够明确返回的数据类型,避免类型不一致的问题。

5. 多泛型参数

有时,一个类或函数需要多个泛型参数。可以通过传入多个泛型实现更复杂的逻辑。

案例:映射类型

class KeyValuePair<K, V> {
  constructor(public key: K, public value: V) {}
}

// 使用
const kv1 = new KeyValuePair<string, number>("Age", 25);
const kv2 = new KeyValuePair<number, boolean>(1, true);

console.log(kv1.key, kv1.value); // 输出: 'Age', 25
console.log(kv2.key, kv2.value); // 输出: 1, true

分析

  • KV 分别表示键和值的类型,可以独立指定。
  • 通过多泛型参数,类的灵活性和适应性更高。

6. 泛型工具类的场景

在实际开发中,常见的场景包括数据处理、对象管理等,泛型可以帮助我们实现高复用性代码。

案例:简单的队列类

class Queue<T> {
  private items: T[] = [];

  enqueue(item: T): void {
    this.items.push(item);
  }

  dequeue(): T | undefined {
    return this.items.shift();
  }

  isEmpty(): boolean {
    return this.items.length === 0;
  }

  size(): number {
    return this.items.length;
  }
}

// 使用
const queue = new Queue<number>();
queue.enqueue(1);
queue.enqueue(2);
queue.enqueue(3);

console.log(queue.dequeue()); // 输出: 1
console.log(queue.size());    // 输出: 2

应用场景

  • 任务管理:队列可以用来管理任务的顺序执行。
  • 数据流处理:在需要按顺序处理的数据流中,比如消息队列。

总结

在 TypeScript 中,泛型是一种强大的工具,可以提升代码的灵活性、复用性和类型安全性。通过结合类、接口和函数,泛型在实际开发中有广泛的应用场景。尤其是在需要处理不同类型的数据时(比如 API 返回值、通用存储类等),泛型提供了一种优雅的解决方案。掌握泛型的使用方法,能让我们在开发中写出更清晰、健壮的代码。