【Typescript】07-泛型入门与实战
泛型入门与实战很多人第一次看到泛型都会觉得它有点抽象。代码里突然多出一个T、K、V好像进入了另一个层级。其实泛型的本质非常朴素让类型也能像函数参数一样被传入。如果说前面的类型声明解决的是“这个东西是什么”那么泛型要解决的是“这段逻辑对很多种类型都成立但我仍然希望保留它们之间的关系”。它不是一种炫技工具而是 TypeScript 从静态标注走向类型抽象的关键一跃。为什么我们需要泛型先看一个没有泛型的例子functiongetFirstAny(list:any[]){returnlist[0];}这段代码当然能工作但问题很明显你传入的是字符串数组也好用户对象数组也好返回值都会被推成any。后面的类型信息直接断掉。再看泛型版本functiongetFirstT(list:T[]):T{returnlist[0];}如果你传入string[]返回值就是string传入number[]返回值就是number传入User[]返回值就是User。这就是泛型真正有价值的地方它保留了输入和输出之间的类型关系。最简单的泛型函数functionidentityT(value:T):T{returnvalue;}这里的T就是类型参数。它不是具体类型而是一个“稍后再决定”的类型占位符。调用时你可以显式写出constaidentitystring(hello);也可以交给 TypeScript 推断constbidentity(123);大多数情况下推断已经足够好。你真正需要关心的是T在函数内部和外部之间建立了什么关系。泛型不是为了灵活而是为了“带约束地灵活”这一点非常关键。很多人会觉得“反正泛型就是更灵活”但真正准确的理解应该是泛型让代码在保持类型安全的前提下获得灵活性。它和any最大的区别就是any会抹掉关系泛型会保留关系所以泛型不是any的高级版而是另一种完全不同的思想。一个更接近实际的例子包装返回值functionwrapT(value:T){return{value};}当你调用constwrappedUserwrap({id:1,name:Alice});TypeScript 会自动推断出{value:{id:number;name:string;};}这类模式在请求封装、状态管理、缓存系统、表单工具里非常常见。泛型约束不是任意类型都可以有时候你不是想要“任何类型都能进来”而是“任何满足某个条件的类型都能进来”。这时就要用约束。functionprintLengthTextends{length:number}(value:T){console.log(value.length);}这里的extends不是类继承而是说T至少要具备length: number这个结构。因此下面这些都可以printLength(hello);printLength([1,2,3]);但下面这种就不行printLength(123);因为数字没有length。泛型约束解决的是“抽象不能脱离现实”很多初学者一接触泛型就会把它理解成“无限自由的占位符”。这其实容易走偏。泛型真正成熟的用法往往不是无限泛而是在抽象中保留必要边界。比如你做一个排序函数可能只接受可比较的值你做一个列表函数可能要求元素具备某个主键你做一个缓存函数可能要求 key 是字符串。这些都是泛型约束的用武之地。泛型接口与泛型类型别名泛型不仅能写在函数上也能写在接口和类型别名上interfaceApiResponseT{code:number;data:T;message:string;}typePageResultT{list:T[];total:number;};这两种定义在真实项目里极其常见。比如ApiResponseUserApiResponseOrder[]PageResultArticlePageResultComment它们的共同点是整体结构固定只有其中某一部分随场景变化。分页接口为什么天然适合泛型因为分页结果通常长这样typePageResultT{list:T[];total:number;page:number;pageSize:number;};不管你分页的是用户、订单还是文章分页外壳都是一样的变化的只是list里的元素类型。这个“固定外壳 可变化内容”的结构几乎就是泛型最标准的使用场景。多个类型参数不是为了复杂而是为了表达多段关系functionmapToObjectKextendsstring,V(key:K,value:V):RecordK,V{return{[key]:value}asRecordK,V;}这里同时用了两个类型参数K表示键V表示值这不是为了炫技而是因为这里确实有两套不同的类型关系要表达。你应该把多个类型参数理解成“多条关系线”而不是“语法更高级”。一旦函数里涉及多个位置、多个输入、多个输出之间的对应关系多个类型参数就很自然。泛型默认类型再进一点你还会看到这种写法typeApiResponseTunknown{code:number;data:T;message:string;};这里T unknown表示如果调用方没有显式指定类型参数就默认用unknown。这种写法在库设计里很常见能在灵活性和安全性之间提供一个更稳妥的默认值。泛型类也存在但别过度使用classBoxT{constructor(publicvalue:T){}getValue():T{returnthis.value;}}泛型类并不少见但在业务代码里泛型函数和泛型类型通常比泛型类更常用。原因也很简单很多场景只需要表达输入输出关系不一定需要实例化对象。什么时候不该用泛型这是一个特别重要的判断能力。不是只要能写成泛型就应该写成泛型。如果一个函数只处理一种固定类型例如functionformatPrice(price:number):string{return¥${price.toFixed(2)};}那就没必要硬改成泛型。很多初学者一学会T就想把所有函数都写成泛型结果代码里到处都是没有实际意义的类型参数。好的泛型通常满足两个条件这段逻辑确实对多种类型都成立类型参数能表达明确、可复用的关系初学者常见误区误区一把泛型当作“高级的 any”泛型不是给你偷懒用的它是为了保留关系而不是为了逃避具体类型。误区二看见可复用就立刻上泛型如果抽象层次太早类型参数名会满天飞反而让代码更难读。泛型应该解决真实重复而不是想象中的未来复用。误区三类型参数命名混乱简单场景用T、K、V没问题但一旦语义明显命名更具体往往更好比如TData、TItem、TKey。可读性比“写得短”更重要。本文小结泛型不是让代码更复杂而是让可复用逻辑在不丢失类型关系的前提下保持灵活。它解决的核心问题不是“这个值是什么”而是“当输入变化时输出应该如何随之变化”。真正掌握泛型之后你看待类型系统的方式会发生变化。你不再只是给现有结构贴标签而是开始设计类型之间的关系网。这也是 TypeScript 从“会写标注”走向“会做抽象”的关键一步。练习写一个泛型函数wrapT返回{ value: T }并分别传入字符串、数字和对象测试推断结果。写一个泛型类型StateT包含loading、data、error三个字段并为用户列表和文章列表分别实例化。思考分页接口、缓存容器、请求响应包装器为什么几乎天然适合用泛型后记2026年5月21日于上海。