TypeScript学习-第7章:泛型(Generic)
各位前端小伙伴,是不是曾遇到过这样的窘境:想写个通用工具函数,要么用 any 摆烂(事后调试火葬场),要么为 string、number、object 各写一套重复代码(Ctrl+C/V 到麻木)?别急,TypeScript 给我们准备了"类型复用神器"------泛型(Generic),既能实现一套代码适配多种类型,又能保住类型安全的底线。今天咱们就用轻松的姿势,吃透泛型的核心玩法,顺便摆脱"重复造轮子"的命运。
一、泛型核心思想:给代码加个"类型万能接口"
先一句话搞懂泛型:泛型允许我们在定义函数、接口、类时,不预先指定具体类型,而是在使用时再动态传入类型参数。就像奶茶店的"万能杯",既能装珍珠奶茶,也能装水果茶,杯子本身不限制内容,但你装什么,它就对应什么口味------泛型就是给代码加了这样的"万能容器"。
没有泛型时,我们要么妥协类型安全,要么硬扛重复代码:
typescript
// 用any摆烂:丢失类型校验,返回值变成any,后续操作易出错
function getValue(arg: any): any {
return arg;
}
const str = getValue("hello"); // str类型是any,不是string
// 重复造轮子:为不同类型写相同逻辑,维护成本爆炸
function getString(arg: string): string { return arg; }
function getNumber(arg: number): number { return arg; }
有了泛型,一套代码搞定所有类型,还能保住类型安全,简直是"鱼和熊掌兼得":
typescript
// 泛型函数:<T>是类型参数,相当于给类型起了个"占位符"
function getValue<T>(arg: T): T {
return arg;
}
// 使用时传入具体类型(也可省略,TS自动推导)
const str = getValue<string>("hello"); // str类型是string
const num = getValue(123); // 自动推导T为number,num类型是number
核心优势:泛型不是 any 的替代品!any 会丢失类型信息,而泛型会"记住"传入的类型,全程保持类型校验,这就是"类型安全的复用"。
二、泛型基础玩法:从函数到接口/类
1. 泛型函数:最常用的入门场景
泛型函数的核心是 <T>(类型参数,名字可自定义,常用 T、U、V 等),它就像一个"类型变量",在函数定义时占位,使用时赋值。除了单个类型参数,还支持多个参数组合:
typescript
// 多个类型参数:适配"键值对"场景
function createPair<T, U>(key: T, value: U): [T, U] {
return [key, value];
}
const pair1 = createPair("name", "张三"); // [string, string]
const pair2 = createPair(1, "age"); // [number, string]
这里的 T 和 U 是独立的类型参数,可分别传入不同类型,灵活性拉满。
2. 泛型接口/类:给复杂类型加"万能模板"
当我们需要定义可复用的接口或类时,泛型同样适用。比如定义一个"快递盒"接口,既能装手机,也能装零食,用泛型就能轻松实现:
typescript
// 泛型接口:Box<T>表示"装T类型物品的盒子"
interface Box<T> {
value: T;
getValue: () => T;
}
// 使用时指定类型,实现不同的盒子
const phoneBox: Box<string> = {
value: "iPhone 15",
getValue: () => phoneBox.value
};
const snackBox: Box<string[]> = {
value: ["薯片", "巧克力"],
getValue: () => snackBox.value
};
泛型类的用法类似,适合创建可适配多种类型的类实例,比如一个通用的"栈"结构:
typescript
// 泛型类:Stack<T>表示"存储T类型元素的栈"
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
}
// 数字栈
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
// 字符串栈
const stringStack = new Stack<string>();
stringStack.push("hello");
三、泛型进阶:约束、默认值与工具
1. 泛型约束:给"万能模板"加边界
有时候我们不希望泛型接收"任意类型",而是限制在特定范围(比如必须包含某个属性),这时候就需要用 extends 做泛型约束。比如写一个"获取对象属性"的函数,要求传入的对象必须有对应的属性:
typescript
// 约束T必须是"包含name属性的对象"
function getName<T extends { name: string }>(obj: T): string {
return obj.name;
}
// 合法:对象有name属性
getName({ name: "张三", age: 25 });
// 报错:对象没有name属性,不符合约束
getName({ age: 25 }); // 提示:类型"{ age: number; }"缺少属性"name"
也可以约束泛型继承自某个接口或类,实现更精准的类型控制:
typescript
interface HasId {
id: number;
}
// 约束T必须继承HasId接口
function getId<T extends HasId>(obj: T): number {
return obj.id;
}
2. 泛型默认值:给类型参数"兜底选项"
和函数参数可以设默认值一样,泛型也能指定默认类型,当使用时未传入类型参数,就会使用默认值。比如定义一个"默认存储数字"的数组工具:
typescript
// 泛型默认值:T默认是number
function createArray<T = number>(length: number, value: T): T[] {
return Array(length).fill(value);
}
// 未传入类型,默认T为number
const numArr = createArray(3, 0); // number[]
// 传入类型,覆盖默认值
const strArr = createArray<string>(3, "hello"); // string[]
3. 泛型工具:typeof与keyof的强强联合
TypeScript 提供了 typeof(获取变量类型)和keyof(获取对象所有键名)两个工具,和泛型搭配使用,能实现更灵活的类型推导。
-
keyof:获取对象类型的所有键名,返回联合类型,常和泛型约束搭配;
-
typeof:根据变量推导类型,避免重复定义接口。
typescript
// 1. keyof + 泛型:安全获取对象属性
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "张三", age: 25 };
getProp(user, "name"); // 合法,返回string
getProp(user, "gender"); // 报错:"gender"不是user的键
// 2. typeof + 泛型:根据变量推导类型
const product = { id: 1, name: "耳机", price: 999 };
// 用typeof推导product的类型,赋值给T
function copyProduct<T extends typeof product>(obj: T): T {
return { ...obj };
}
四、泛型实战:封装通用工具函数
学完基础用法,咱们用泛型封装两个常用工具函数,感受"一次编写,处处复用"的快乐。
1. 通用数组过滤函数
适配任意类型数组,根据传入的条件过滤元素,保持返回值类型和原数组一致:
typescript
// 泛型过滤函数:T为数组元素类型
function filterArray<T>(arr: T[], condition: (item: T) => boolean): T[] {
return arr.filter(condition);
}
// 过滤数字数组
const numbers = [1, 2, 3, 4, 5];
const evenNumbers = filterArray(numbers, (num) => num % 2 === 0); // number[]
// 过滤对象数组
const users = [
{ name: "张三", age: 25 },
{ name: "李四", age: 18 }
];
const adults = filterArray(users, (user) => user.age >= 18); // { name: string; age: number; }[]
2. 简易泛型深拷贝函数
适配对象、数组等复杂类型,实现深拷贝,同时保持原类型:
typescript
function deepClone<T>(obj: T): T {
// 处理null和基本类型
if (obj === null || typeof obj !== "object") {
return obj;
}
// 处理数组
if (Array.isArray(obj)) {
return obj.map(item => deepClone(item)) as T;
}
// 处理对象
const clonedObj: Record<string, any> = {};
for (const key in obj) {
if (obj.hasOwnProperty(key)) {
clonedObj[key] = deepClone(obj[key]);
}
}
return clonedObj as T;
}
// 测试:对象深拷贝,类型保持一致
const user = { name: "张三", age: 25, hobbies: ["篮球", "游戏"] };
const clonedUser = deepClone(user);
clonedUser.hobbies.push("读书");
console.log(user.hobbies); // ["篮球", "游戏"](原对象不受影响)
五、泛型避坑指南(深度总结)
泛型虽好用,但也容易踩坑,这几点一定要注意:
-
别过度泛型化:如果函数/类只适配1-2种类型,没必要强行用泛型,反而会增加代码复杂度;
-
泛型约束要精准:约束太宽松会丢失类型安全,太严格会限制灵活性,根据实际场景调整;
-
避免泛型与any混淆:泛型是"类型安全的复用",any是"放弃类型校验",二者本质不同;
-
利用TS自动推导:大部分场景下,TS能自动推导泛型类型,无需手动传入,简化代码。
最后再总结一句:泛型的核心不是"复杂的语法",而是"类型复用的思想"------它让我们的代码既能适配多种场景,又能守住TypeScript的类型安全底线,是从"能跑"到"写好"的关键一步。