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