1. 你以前怎么惨你写了一个函数接收一个值把它包进数组里返回。functionwrapInArray(value:string):string[]{return[value];}隔壁同事也要用。但他传进来的是number。你复制了一份functionwrapNumberInArray(value:number):number[]{return[value];}老板又要一个boolean版本。你开始复制第三份。HR 找你谈话说代码审查没过。问题不是同事传错了类型。问题是你的函数写死了类型。类型不安全的时候你只能复制粘贴。类型安全了你又陷入了复制粘贴的另一种形式——只不过这次编译器逼你复制。泛型解决这个问题写一个函数让它自动适应传给它的类型。2. 先看一眼烂代码// 三个函数逻辑一模一样只是类型标注不同functionwrapString(value:string):string[]{return[value];}functionwrapNumber(value:number):number[]{return[value];}functionwrapBoolean(value:boolean):boolean[]{return[value];}// 更糟糕的用 any 逃避functionwrapAny(value:any):any[]{return[value];}// any 让你输掉所有类型信息constresultwrapAny(hello);// result 的类型是 any[]。TS 不知道里面是 string。// result[0].toUpperCase() —— 编译通过运行也通过因为是 string// result[0].toFixed(2) —— 编译也通过因为 any 跳过了所有检查// 运行时崩溃toFixed is not a function用any就像在超市里把所有人的购物车混在一起——你不知道谁的购物车里有鸡蛋谁的有砖头。出问题了才发现。3. 用新语法修好它3.1 泛型函数T泛型类比自动售货机。投可乐进去出来可乐。投薯片进去出来薯片。售货机不关心你投什么但它保证出来的东西和你投的一样。// T 声明一个类型变量 T。调用时TS 根据你传入的值推导出 T 是什么。functionwrapInArrayT(value:T):T[]{return[value];}conststringswrapInArray(hello);// strings: string[]constnumberswrapInArray(42);// numbers: number[]constbooleanswrapInArray(true);// booleans: boolean[]// TS 记住了入参类型和返回类型的对应关系。// strings[0].toUpperCase() ✅// strings[0].toFixed(2) ❌ 编译报错T只是一个名字。叫TValue、叫Item、随便你。当函数只有一个泛型参数时惯例用T。你也能显式指定类型参数constexplicitwrapInArraystring(hello);但绝大多数时候没必要——TS 能自己推导出来。3.2 泛型约束T extends ...有时你不接受任意类型。你只接受有某个属性的类型。// 需求接收一个对象返回它的某个属性值// 不约束的话TS 不知道 T 有没有 length 属性functiongetLengthT(obj:T):number{returnobj.length;// ❌ Property length does not exist on type T}用extends约束 T// T extends { length: number } —— T 必须是有 length 属性的类型functiongetLengthTextends{length:number}(obj:T):number{returnobj.length;// ✅ TS 知道 obj 一定有 length}getLength(hello);// ✅ string 有 lengthgetLength([1,2,3]);// ✅ 数组有 lengthgetLength({length:10});// ✅// getLength(42); // ❌ number 没有 length泛型约束 “这台售货机只接受饮料不接受石头”。一个更实用的例子// 确保传入的对象至少有 name 字段functiongreetTextends{name:string}(obj:T):string{returnHello,${obj.name}!;}greet({name:Alice,age:30});// ✅ 多出来的属性没关系// greet({ age: 30 }); // ❌ 没有 name3.3 泛型接口接口也可以用泛型。标准库的ArrayT和MapK, V就是泛型接口。// 一个简单的键值对容器interfaceBoxT{value:T;}conststringBox:Boxstring{value:hello};constnumberBox:Boxnumber{value:42};// 更实用的例子API 响应包装interfaceApiResponseT{data:T;status:number;message:string;}typeUserResponseApiResponse{id:number;name:string};typePostResponseApiResponse{title:string;body:string};// 标准库示例constnames:Arraystring[Alice,Bob];constscores:Mapstring,numbernewMap();scores.set(Alice,95);3.4 默认泛型参数有些泛型参数 90% 的情况都是同一个类型。给个默认值。// 大多数时候事件数据就是一个简单的字符串interfaceEventTstring{type:string;data:T;}constsimple:Event{type:click,data:clicked};// data: stringconstcomplex:Event{x:number}{type:move,data:{x:100}};// data: { x: number }默认参数让你不写string也能得到默认行为但需要时还能覆盖。4. 什么时候不该用函数只接受一个具体类型。别写成function addT(a: T, b: T)再在内部硬转成 number。直接写function add(a: number, b: number)。泛型参数没有出现在参数列表中。function createT(): T——你能凭空造出一个 T 吗不能。那就在骗 TS。这种几乎总是反模式。用泛型替换简单的联合类型。如果你知道参数只可能是string | number写联合类型比泛型更清晰。为了酷而泛型。每个类型参数都增加认知负担。如果一个问题没有重复的类型逻辑不需要泛型。5. 小结语法一句话示例T声明类型变量输入和输出类型联动function idT(x: T): T { return x; }T extends U约束 T 必须是 U 的子类型function lenT extends { length: number }(x: T): numberInterfaceT接口接受类型参数interface BoxT { value: T }T U泛型默认值interface EventT string { data: T }ArrayT标准库的泛型数组const a: Arraynumber [1, 2]MapK, V标准库的泛型 Mapconst m: Mapstring, number new Map()