TypeScript泛型详解与使用建议
泛型(Generics)是TypeScript中用于增强代码复用性和类型安全的核心特性。它允许我们在定义函数、类、接口时使用类型变量,在调用时动态指定具体类型,从而实现逻辑复用同时保持严格的类型检查。
一、泛型核心概念
泛型本质是类型参数化,类似于函数参数。基本语法使用尖括号<>
声明类型变量,常用T
(Type)、U
、V
等命名:
// 函数泛型
function identity<T>(arg: T): T {
return arg;
}
// 调用时显式指定类型
const str = identity<string>("hello"); // str类型为string
// 或依赖类型推断
const num = identity(42); // num类型为number
二、提高代码复用性的典型场景
- 通用数据处理函数
避免为不同类型重复编写相同逻辑:
// 非泛型:需为每个类型写单独函数
function printStringArray(arr: string[]) {
arr.forEach(item => console.log(item));
}
function printNumberArray(arr: number[]) {
arr.forEach(item => console.log(item));
}
// 泛型方案:一个函数处理所有类型数组
function printArray<T>(arr: T[]): void {
arr.forEach(item => console.log(item));
}
printArray<string>(["a", "b"]); // 显式指定类型
printArray([1, 2, 3]); // 自动推断为number[]
- API响应结构封装
统一接口返回格式,动态指定data
类型:
interface ApiResponse<T> {
code: number;
data: T; // 核心数据字段动态类型
message: string;
}
// 用户数据接口
interface User {
id: number;
name: string;
}
// 使用泛型接口
const userResponse: ApiResponse<User> = {
code: 200,
data: { id: 1, name: "Alice" },
message: "OK"
};
- 类中的类型复用
创建可重用的容器类,保存任意类型值:
class Box<T> {
private value: T;
constructor(initialValue: T) {
this.value = initialValue;
}
getValue(): T {
return this.value;
}
setValue(newValue: T): void {
this.value = newValue;
}
}
// 存储数字
const numberBox = new Box<number>(42);
console.log(numberBox.getValue()); // 42
// 存储对象
const objBox = new Box({ name: "Bob" });
objBox.setValue({ name: "Charlie" }); // 类型自动推断为{ name: string }
三、高级技巧与使用建议
- 类型约束(Type Constraints)
使用extends
关键字限制泛型类型必须满足特定结构:
// 确保传入对象有length属性且为number类型
function logLength<T extends { length: number }>(obj: T): void {
console.log(obj.length);
}
logLength("hello"); // 5
logLength([1, 2, 3]); // 3
// logLength(42); // 错误:number没有length属性
- 默认类型参数
为泛型提供默认类型,简化调用:
interface Pagination<T = unknown> {
current: number;
pageSize: number;
data: T; // 默认类型为unknown
}
// 不指定类型时使用默认值
const page: Pagination = {
current: 1,
pageSize: 10,
data: {} // 类型推断为unknown
};
// 显式指定类型
const userPage: Pagination<User[]> = {
current: 2,
pageSize: 20,
data: [{ id: 1, name: "Alice" }]
};
- 泛型与异步请求
封装通用数据请求函数,自动解析响应类型:
async function fetchData<T>(url: string): Promise<T> {
const response = await fetch(url);
return response.json(); // 自动推断为T类型
}
// 使用示例
interface Post {
id: number;
title: string;
}
// 获取帖子列表
const posts = await fetchData<Post[]>("/api/posts");
// posts类型为Post[]
四、实际开发注意事项
- 避免过度泛型化
简单场景无需泛型,优先使用具体类型:
// 错误示范:没必要用泛型
function add<T>(a: T, b: T): T {
return a + b; // 这里会报错,因为TS不知道T是否支持+
}
// 正确做法:明确限制为number
function add(a: number, b: number): number {
return a + b;
}
- 优先依赖类型推断
大多数情况不需要显式指定类型参数:
// 冗余写法
const arr = Array<string> = ["a", "b"];
// 推荐写法
const arr = ["a", "b"]; // 自动推断为string[]
- 谨慎处理复杂嵌套
多层泛型可能降低可读性,适当拆分:
// 难以理解的嵌套泛型
type DeepNested<T> = Promise<Array<{ data: T[] }>>;
// 建议拆解
interface Wrapper<T> {
data: T[];
}
type BetterType<T> = Promise<Wrapper<T>[]>;
- 注意类型收缩问题
联合类型与泛型结合时可能需要类型守卫:
function process<T extends string | number>(value: T): T {
if (typeof value === "string") {
return value.trim() as T; // 需要类型断言
}
return value.toFixed(2) as T;
}
五、总结
合理使用泛型可以显著提升代码复用率和类型安全性,但需注意:
- 在数据处理函数、集合类、工具类型等场景优先考虑泛型
- 通过
extends
约束和默认参数优化使用体验 - 避免在简单场景或导致类型复杂化的地方使用
- 结合类型推断减少冗余代码
通过示例中的模式,开发者可以在保持类型安全的前提下,构建灵活且易于维护的TypeScript代码库。