泛型:像“填空”一样写类型,让你的代码从“复制粘贴”中解放

张开发
2026/4/9 20:44:15 15 分钟阅读

分享文章

泛型:像“填空”一样写类型,让你的代码从“复制粘贴”中解放
你是不是遇到过这种场景写了一个函数处理数字的版本写一遍处理字符串的版本再写一遍处理数组的又写一遍……最后代码里全是长得差不多的“双胞胎”。今天我们来学TypeScript的泛型——一个能让你写一次、处处用的“类型模板”。从此告别复制粘贴做个体面的程序员。前言想象一下你开了一家“万能快递公司”。客户要寄书你准备书盒要寄衣服你准备衣服盒要寄手机你准备手机盒……每种物品都要单独设计盒子累不累更好的做法设计一种可调节大小的盒子客户说寄什么你就把盒子调成对应大小。这个“可调节的盒子”就是泛型。TypeScript的泛型让你在定义函数、类、接口时先“留个空”等用的时候再往里填具体类型。这样既保证了类型安全又避免了重复代码。一、泛型长啥样一个简单的例子先看一个没有泛型的“悲惨世界”// 只能处理数字functionidentityNumber(arg:number):number{returnarg;}// 只能处理字符串functionidentityString(arg:string):string{returnarg;}// 要处理布尔值再写一个……用泛型只需要一个functionidentityT(arg:T):T{returnarg;}这里的T就像个“占位符”你调用时可以指定具体类型letoutput1identitystring(hello);// 类型是 stringletoutput2identitynumber(123);// 类型是 number但TS很聪明能自动推断所以通常可以省略letoutputidentity(hello);// TS推断出T为string二、泛型不只是“传进去又返回来”你可以约束参数的类型关系。比如你想让函数接收一个数组并返回数组的第一个元素functiongetFirstT(arr:T[]):T{returnarr[0];}constfirstNumbergetFirst([1,2,3]);// 类型 numberconstfirstStringgetFirst([a,b]);// 类型 stringT帮我们保持了“数组元素类型”和“返回值类型”的一致性。三、泛型约束给“占位符”画个圈有时候你不能让T为所欲为。比如你想写一个函数打印参数的length属性functionlogLengthT(arg:T):T{console.log(arg.length);// ❌ 报错T可能没有lengthreturnarg;}因为T可能是number、boolean它们没有.length。这时候需要约束——告诉TST必须是有length属性的类型。interfaceHasLength{length:number;}functionlogLengthTextendsHasLength(arg:T):T{console.log(arg.length);// ✅ 安全returnarg;}logLength(hello);// 字符串有lengthlogLength([1,2,3]);// 数组有lengthlogLength(123);// ❌ 数字没有length报错extends关键字在这里不是继承而是“约束为某个类型的子集”。四、泛型接口把接口变成“模具”接口也可以泛型化比如定义一个通用的响应结构interfaceApiResponseT{code:number;message:string;data:T;}// 使用typeUser{name:string;age:number};constresponse:ApiResponseUser{code:200,message:success,data:{name:张三,age:18}};这样你就能用一个接口描述所有API返回格式只需替换T。五、泛型类像造模具一样造类类同样可以泛型classQueueT{privatedata:T[][];push(item:T){this.data.push(item);}pop():T|undefined{returnthis.data.shift();}}constnumberQueuenewQueuenumber();numberQueue.push(123);numberQueue.push(456);// ❌ 报错只能放数字六、泛型工具类型TS内置的“变形金刚”TS提供了一些内置的泛型工具能帮你快速转换类型。1.PartialT把属性都变成可选interfaceUser{name:string;age:number;}typePartialUserPartialUser;// { name?: string; age?: number; }2.ReadonlyT把所有属性变成只读typeReadonlyUserReadonlyUser;// { readonly name: string; readonly age: number; }3.PickT, K从T中挑选部分属性typeUserNamePickUser,name;// { name: string; }4.OmitT, K从T中排除部分属性typeUserWithoutAgeOmitUser,age;// { name: string; }还有RecordK, T、Exclude、Extract等遇到具体场景时再查文档即可。七、联合类型与交叉类型不是泛型但常一起用联合类型|这个或那个letvalue:string|number;valuehello;// OKvalue123;// OKvaluetrue;// ❌联合类型适合“不确定具体是哪个但知道是有限的几种”。交叉类型既要又要interfaceName{name:string;}interfaceAge{age:number;}typePersonNameAge;// 同时有name和age属性constp:Person{name:张三,age:18};交叉类型常用来合并多个类型。八、类型保护让TS相信你当你使用联合类型时TS会限制你只能调用所有类型共有的方法。要调用特定类型的方法需要类型保护。functionprintLength(value:string|number){// console.log(value.length); // ❌ 报错number没有lengthif(typeofvaluestring){console.log(value.length);// ✅ 这里TS知道value是string}else{console.log(value.toFixed(2));}}除了typeof还有instanceof、in关键字、自定义类型守卫。九、实战用泛型写一个“万能”的缓存函数interfaceCacheT{get(key:string):T|undefined;set(key:string,value:T):void;}functioncreateCacheT():CacheT{conststore:Recordstring,T{};return{get(key){returnstore[key];},set(key,value){store[key]value;}};}conststringCachecreateCachestring();stringCache.set(name,张三);constnamestringCache.get(name);// 类型是 string | undefinedconstnumberCachecreateCachenumber();numberCache.set(age,18);看一套代码同时服务了字符串缓存和数字缓存类型还完全安全。十、总结泛型是“类型编程”的起点泛型就是“类型的参数”让组件函数、类、接口能适应多种类型同时保留类型关系。约束用extends限定泛型的范围。泛型接口/类让数据结构通用。内置工具类型Partial、Pick等简化常见类型转换。联合类型表示“或”交叉类型表示“且”类型保护用来区分联合中的具体类型。掌握泛型你就能写出更抽象、更复用、更安全的代码。明天我们将继续TypeScript的高级主题——装饰器看看这个类似Java注解的特性如何在TS里玩出花样。如果你觉得今天的“万能模具”讲得通透点个赞让更多人看到。明天我们聊聊装饰器——那个在Angular和NestJS里无处不在的黑魔法。我们明天见

更多文章