TypeScript 泛型使用实践记录

78 阅读5分钟

在 TypeScript 中,泛型是一个强大的工具,它允许你在编写类、函数或接口时,保持代码的灵活性和类型安全。泛型使得类型在使用时才被确定,而不是在定义时就固定,从而提高了代码的复用性和可维护性。

简析泛型

1. 泛型的基础使用

泛型通常是定义类、函数或接口时,作为一个占位符,表示该位置会在使用时被具体的类型替换。下面是一个简单的泛型函数示例:

// 泛型函数:返回传入值的类型
function identity<T>(arg: T): T {
  return arg;
}

let result1 = identity(42); // T 会被推断为 number
let result2 = identity("hello"); // T 会被推断为 string

在这个示例中,T 是一个类型参数,函数 identity 接受一个参数 arg,并返回相同类型的值。TypeScript 会自动推断 T 的类型,也可以显式地指定类型。

2. 泛型类

泛型类允许我们在定义类时使用类型参数。这种方式可以使得类的操作对象更加灵活,增强代码的复用性。

// 泛型类
class Box<T> {
  private value: T;

  constructor(value: T) {
    this.value = value;
  }

  getValue(): T {
    return this.value;
  }

  setValue(value: T): void {
    this.value = value;
  }
}

let numberBox = new Box(123); // T 被推断为 number
let stringBox = new Box("hello"); // T 被推断为 string

console.log(numberBox.getValue()); // 输出 123
console.log(stringBox.getValue()); // 输出 hello

3. 泛型约束

有时我们希望泛型的类型参数能够满足某些条件,例如,类型参数必须拥有某个方法或者属性。可以通过 extends 关键字来为泛型添加约束。

// 泛型约束
interface LengthWise {
  length: number;
}

function logLength<T extends LengthWise>(item: T): void {
  console.log(item.length);
}

logLength("hello"); // 输出 5,因为字符串有 length 属性
logLength([1, 2, 3]); // 输出 3,因为数组有 length 属性
// logLength(42); // 错误,因为数字没有 length 属性

在这个示例中,T extends LengthWise 表示泛型 T 必须满足 LengthWise 接口的结构,即包含一个 length 属性。

4. 泛型与接口

接口也可以使用泛型,通常用于定义一种结构,其中类型会在使用时被具体化。泛型接口使得接口更加灵活。

// 泛型接口
interface Pair<T, U> {
  first: T;
  second: U;
}

let pair1: Pair<number, string> = { first: 1, second: "apple" };
let pair2: Pair<string, boolean> = { first: "hello", second: true };

在这里,Pair 接口接受两个类型参数 TU,并用它们来定义 firstsecond 属性的类型。

5. 泛型约束与继承

通过组合接口、类与泛型约束,可以实现更复杂的类型逻辑。泛型类型不仅可以继承接口,还可以进行多层嵌套。

// 泛型约束与继承
class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

class Dog extends Animal {
  breed: string;
  constructor(name: string, breed: string) {
    super(name);
    this.breed = breed;
  }
}

function printAnimalName<T extends Animal>(animal: T): void {
  console.log(animal.name);
}

let myDog = new Dog("Buddy", "Golden Retriever");
printAnimalName(myDog); // 输出 Buddy

在这个例子中,T 继承自 Animal 类,这确保了传入的参数一定具有 name 属性。

6. 高级泛型用法

条件类型

TypeScript 提供了条件类型,可以根据泛型的类型在编译时做出决策,进一步增强灵活性。

// 条件类型
type IsString<T> = T extends string ? "Yes" : "No";

type Test1 = IsString<string>; // "Yes"
type Test2 = IsString<number>; // "No"

映射类型

映射类型可以基于给定的类型生成一个新的类型。结合泛型,可以实现更加动态的类型定义。

// 映射类型
type ReadOnly<T> = {
  readonly [P in keyof T]: T[P];
};

interface Person {
  name: string;
  age: number;
}

type ReadOnlyPerson = ReadOnly<Person>;
// ReadOnlyPerson 的属性是只读的

7. 泛型的实际应用场景

  • 数据结构:在实现栈、队列、链表等数据结构时,使用泛型可以确保元素类型的安全性。
  • API 调用:在与后端接口交互时,使用泛型来定义 API 响应的类型,使得数据处理更加安全。
  • 表单处理:对于动态表单,泛型可以根据表单字段类型定义动态表单值类型。
  • 组件库:在实现组件库时,泛型可以根据组件的使用场景灵活地定义 props 和 state 的类型。

示例:API 调用与泛型

1. 定义 API 响应类型接口

我们首先定义几个接口,表示不同的 API 响应类型。

// 用户信息接口
interface User {
  id: number;
  name: string;
  email: string;
}

// 商品信息接口
interface Product {
  id: number;
  name: string;
  price: number;
  stock: number;
}

// API 响应结构通用接口
interface ApiResponse<T> {
  data: T;
  message: string;
  status: number;
}
  • UserProduct 是两个不同的 API 响应类型。
  • ApiResponse<T> 是一个通用接口,表示 API 响应的数据结构。T 是泛型,表示响应数据的具体类型。

2. 创建泛型 API 调用函数

接下来,我们定义一个通用的 API 调用函数,接受泛型类型参数 T,并返回一个 Promise,该 Promise 会解析为 API 响应的数据。

// 模拟 API 调用
async function fetchApiData<T>(url: string): Promise<ApiResponse<T>> {
  try {
    const response = await fetch(url);
    
    // 假设接口返回的是 JSON 格式
    const result: ApiResponse<T> = await response.json();
    
    if (response.ok) {
      return result; // 返回 API 响应数据
    } else {
      throw new Error(result.message || '请求失败');
    }
  } catch (error) {
    // 异常处理
    console.error('API 调用错误:', error);
    throw error;
  }
}

fetchApiData 函数中,我们使用了 T 来表示 API 返回的数据类型。函数返回一个 Promise<ApiResponse<T>>,即一个 ApiResponse 对象,其中 data 字段的类型是 T

3. 使用泛型 API 调用

现在我们可以根据不同的需求,调用 API 并解析返回的 JSON 数据。

// 获取用户信息
async function getUserInfo(userId: number) {
  const url = `https://api.example.com/users/${userId}`;
  const response = await fetchApiData<User>(url);  // 传入 User 类型
  console.log('用户信息:', response.data);
}

// 获取商品列表
async function getProductList() {
  const url = 'https://api.example.com/products';
  const response = await fetchApiData<Product[]>(url);  // 传入 Product[] 类型
  console.log('商品列表:', response.data);
}

// 示例:调用 API 获取数据
getUserInfo(123).catch(error => console.error(error));
getProductList().catch(error => console.error(error));