泛型编程基础

泛型(Generics)是 TypeScript 中实现代码复用与类型安全的核心机制,通过引入类型参数(Type Parameters)使函数、接口或类能够操作多种数据类型而无需牺牲类型检查。理解泛型的价值需从实际问题出发:在处理不同数据类型时,传统方式需为每种类型定义重复逻辑。例如,日志打印函数若分别处理字符串与数字类型,需定义 logString(s: string)logNumber(n: number) 两个函数,导致代码冗余且扩展性差。泛型通过抽象类型参数解决这一问题,使单一实现支持多类型场景。

泛型语法与基础应用

泛型的核心语法是在函数、接口或类定义时引入类型参数,用尖括号 <T> 表示(T 为约定俗成的类型参数名,可自定义)。以通用日志函数为例,其定义如下:

typescript
复制代码
// 泛型函数:接收 T 类型参数并返回 T 类型值
function log<T>(value: T): T {
  console.log(value);
  return value; // 类型参数 T 确保输入与输出类型一致
}

调用时可显式指定类型参数或由 TypeScript 自动推断:

typescript
复制代码
log<string>("hello"); // 显式指定 T 为 string
log(123); // 自动推断 T 为 number,返回类型为 number

泛型语法核心:通过 <类型参数> 声明类型变量,使函数/接口/类具备类型抽象能力。类型参数在定义时作为占位符,使用时被具体类型替换,实现「一次定义,多类型复用」。

类型参数约束

当需要限制类型参数的范围时,可使用 extends 关键字实现约束。例如,获取对象属性的函数需确保传入的键存在于对象中,可通过 keyof 操作符约束键的类型:

typescript
复制代码
// T 为对象类型,K 必须是 T 的键(K extends keyof T)
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]; // 返回值类型为 T 中 K 键对应的值类型
}

// 使用示例
const user = { name: "Alice", age: 30 };
getProperty(user, "name"); // 合法,返回 string 类型
getProperty(user, "height"); // 报错:"height" 不是 user 的键

此约束确保函数仅接受对象存在的键,避免运行时错误,同时保留类型推断能力——返回值类型自动匹配对象属性的实际类型。

泛型接口与类

泛型同样适用于接口和类,使数据结构定义具备通用性。例如,后端 API 响应通常包含状态码与数据,可定义泛型接口统一描述:

typescript
复制代码
// 泛型接口:T 表示响应数据的类型
interface ApiResponse<T> {
  code: number; // 状态码(固定类型)
  data: T; // 数据(泛型类型,随使用场景变化)
}

// 使用示例:用户列表响应
type User = { id: number; name: string };
const userResponse: ApiResponse<User[]> = {
  code: 200,
  data: [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }]
};

泛型类的典型应用是数据结构,如队列、栈等。以下为泛型类的基础定义:

typescript
复制代码
// 泛型类:管理 T 类型元素的集合
class Collection<T> {
  private items: T[] = [];
  
  add(item: T): void {
    this.items.push(item); // 仅允许添加 T 类型元素
  }
  
  get(index: number): T | undefined {
    return this.items[index]; // 返回 T 类型或 undefined
  }
}

前后端泛型实践案例

泛型在前后端开发中均有广泛应用,以下为典型场景:

前端:通用状态管理函数
类似 React 的 useState,泛型确保状态初始值、当前值与更新函数的类型一致性:

typescript
复制代码
// 泛型状态管理函数:T 为状态类型
function useState<T>(initial: T): [T, (value: T) => void] {
  let state = initial; // 初始状态类型为 T
  const setState = (value: T) => { state = value; }; // 更新函数仅接受 T 类型
  return [state, setState]; // 返回 [当前状态, 更新函数],类型均与 T 绑定
}

// 使用示例:管理数字状态
const [count, setCount] = useState<number>(0);
setCount(10); // 合法:number 类型
// setCount("10"); // 报错:类型 "string" 不可分配给 "number"

后端:数据库查询函数
数据库查询结果的类型需动态适配业务模型,泛型使查询函数返回类型与业务实体绑定:

typescript
复制代码
// 泛型查询函数:T 为查询结果的实体类型
async function query<T>(sql: string): Promise<T[]> {
  const result = await db.execute(sql); // 假设 db.execute 执行 SQL 并返回原始数据
  return result as T[]; // 将原始数据断言为 T[] 类型
}

// 使用示例:查询用户列表
type User = { id: number; name: string };
const users = await query<User>("SELECT id, name FROM users");
// users 类型为 Promise<User[]>,支持类型提示与自动补全

实践练习:实现泛型栈(Stack)类

栈是一种遵循「后进先出」(LIFO)原则的数据结构,需支持 push(入栈)和 pop(出栈)操作。通过泛型约束栈内元素类型,确保操作的类型安全:

typescript
复制代码
class Stack<T> {
  private elements: T[] = []; // 存储栈元素的数组,类型为 T[]

  // 入栈:添加 T 类型元素到栈顶
  push(element: T): void {
    this.elements.push(element);
  }

  // 出栈:移除并返回栈顶元素,类型为 T 或 undefined
  pop(): T | undefined {
    return this.elements.pop();
  }

  // 获取栈顶元素(不删除)
  peek(): T | undefined {
    return this.elements[this.elements.length - 1];
  }

  // 判断栈是否为空
  isEmpty(): boolean {
    return this.elements.length === 0;
  }
}

// 使用示例:创建数字类型栈
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
console.log(numberStack.pop()); // 输出 2(类型为 number)

// 创建字符串类型栈
const stringStack = new Stack<string>();
stringStack.push("a");
// stringStack.push(1); // 报错:类型 "number" 不可分配给 "string"

通过泛型参数 TStack 类可适配任意数据类型,同时确保 push 操作仅接受 T 类型元素,pop 操作返回 T 类型元素,实现了代码复用与类型安全的统一。

泛型作为 TypeScript 的核心特性,其价值在于将类型抽象与代码复用结合,使组件具备更强的通用性与可维护性。掌握泛型的语法、约束与应用场景,是构建复杂 TypeScript 应用的基础。