本章定位
上一章,我们已经把 TypeScript 里“给类型起名字”这件事真正建立起来了。
你已经学习了:
interface可以描述稳定的对象结构。type可以给对象、联合类型、函数类型起名字。- 可选属性
?可以表达“这个字段可以没有”。 - 函数类型本质上是在表达“它吃什么、吐什么”。
也就是说,到现在为止,你已经能写出这样的代码:
interfaceUserInfo{userName:string;age:number;}typeSubmitHandler=(value:string)=>void;这一步非常关键。
因为你已经不只是会给变量后面加类型,而是开始会组织“类型结构”了。
但如果你继续往前写,很快又会遇到一个新问题:
有些规则本身是通用的,只是里面装的具体类型不同。
例如:
- “获取数组第一项”这件事,对字符串数组和数字数组都成立
- “列表数据结构”这件事,对商品列表和用户列表都成立
- “接口返回值结构”这件事,对用户数据和商品数据都成立
如果你每次都针对不同类型重新写一份,就会越来越重复。
例如你可能会写出这样的代码:
functiongetFirstNumber(items:number[]):number|undefined{returnitems[0];}functiongetFirstString(items:string[]):string|undefined{returnitems[0];}这里逻辑几乎一样,差别只是:
具体类型不同。
这时候就需要一种能力,帮你表达:
这套规则是通用的,但里面的具体类型可以后面再决定。
这就是泛型最核心的作用。
这一章要解决的核心问题就是:
当同一套类型规则需要在不同数据上重复使用时,TypeScript 该怎样既保留类型信息,又避免重复书写。
本章学习目标
学完这一章后,你应该能做到:
- 理解什么是泛型,以及它到底在解决什么问题。
- 掌握泛型函数的最基础写法。
- 理解尖括号里的
T到底代表什么。 - 理解泛型和
any的本质区别。 - 学会在列表数据结构里使用泛型。
- 学会在接口返回值结构里使用泛型。
- 知道什么时候值得用泛型,什么时候没必要硬上。
- 避开这一阶段最常见的泛型入门坑。
一、为什么会需要“同一套规则,适配不同类型”
先看一个非常直观的问题。
假设你要写一个函数:
返回数组的第一项。
如果你先只考虑数字数组,可能会这样写:
functiongetFirstNumber(items:number[]):number|undefined{returnitems[0];}如果你又想支持字符串数组,可能会接着写:
functiongetFirstString(items:string[]):string|undefined{returnitems[0];}如果以后还要支持布尔数组呢?
functiongetFirstBoolean(items:boolean[]):boolean|undefined{returnitems[0];}1. 这里真正重复的是什么
不是数据,而是:
规则完全一样。
它们本质都在做:
- 接收一个数组
- 返回第 1 项
- 如果没有,就可能是
undefined
唯一变化的,只是数组里装的类型不同。
2. 如果继续这样写,会有什么问题
最直观的问题有三个:
- 重复代码越来越多
- 逻辑明明一样,却要维护很多份
- 每新增一种类型,都要再写一份函数
3. 这时候最自然的需求是什么
就是:
能不能把“返回数组第一项”这套规则只写一份,但让它适配不同类型?
这正是泛型登场的地方。
二、泛型到底是什么
“泛型”这个词第一次听起来,很多人会觉得有点抽象。
你现在可以先把它理解成一句非常直白的话:
先把规则写出来,但里面的具体类型先不写死,等真正使用的时候再决定。
这句话非常重要。
它几乎就是泛型的入门理解核心。
1. 泛型不是“不要类型”
这一点特别关键。
泛型并不是说:
这个地方我懒得管类型了,随便来吧。
恰恰相反,泛型是在表达:
这个地方我很在乎类型,只是这个类型现在还不能提前写死。
2. 泛型不是让规则更模糊,而是让规则更可复用
这也是很多初学者最容易一下子没转过来的地方。
你要慢慢建立的理解是:
泛型并不是放弃规则,而是让同一套规则能够作用在不同类型上。
三、先从最基础的泛型函数开始看
回到刚才“返回数组第一项”的例子。
有了泛型之后,它可以写成这样:
functiongetFirstItem<T>(items:T[]):T|undefined{returnitems[0];}第一次看到这段代码时,很多人会卡在两个地方:
<T>是什么T[]和T | undefined又是什么
别急,我们一点点拆。
1.<T>可以先怎么理解
你可以先把它理解成:
这里先占一个“类型位置”。
也就是说,T不是固定代表字符串,也不是固定代表数字。
它只是一个占位符,表示:
这里有一个类型,会在真正使用这个函数时再确定。
2.items: T[]是什么意思
这表示:
参数
items是一个数组,数组里每一项都是同一种类型T。
3.T | undefined是什么意思
这表示:
返回值要么是数组第 1 项的类型
T,要么如果数组为空,就可能是undefined。
4. 整个函数可以怎么读
你可以直接把它读成:
这是一个通用函数,接收某种类型组成的数组,返回该类型的第一项,如果没有则返回
undefined。
只要你能把这句话说顺,说明泛型的第一层感觉已经开始建立了。
四、T到底是什么,它为什么经常写成一个大写字母
这一节非常重要。
很多纯小白第一次看到泛型时,真正害怕的不是逻辑,而是:
这个
T看起来像什么神秘符号。
其实没有那么复杂。
1.T本质上只是一个名字
你可以把它理解成:
“类型变量”的名字。
就像你在函数里会写:
functionadd(a,b){returna+b;}这里的a、b是“值变量”的名字。
而在泛型里:
<T>这个T则可以先理解成:
“类型变量”的名字。
2. 为什么很多教程都写T
因为这是一种非常常见的约定写法。
T可以理解成:
Type 的首字母。
3.T不是唯一写法
例如你也可能会看到:
<ItemType>或者:
<ValueType>这些都可以。
当前阶段你先记住:
T只是最常见的泛型类型占位符,不是什么固定含义的魔法字母。
五、泛型函数怎么真正用起来
现在你已经知道泛型函数长什么样了。
接下来看看它怎么用。
1. 传入数字数组
functiongetFirstItem<T>(items:T[]):T|undefined{returnitems[0];}constfirstNumber=getFirstItem([10,20,30]);这里 TypeScript 通常会根据你传入的数组内容,推断出:
这次的
T是number
所以firstNumber的类型就会更接近:
number|undefined2. 传入字符串数组
constfirstCourse=getFirstItem(["HTML","CSS","JavaScript"]);这时候 TypeScript 又会推断出:
这次的
T是string
所以返回值类型就会更接近:
string|undefined3. 传入对象数组
constfirstUser=getFirstItem([{userName:"小白",age:18},{userName:"小黑",age:20}]);这时候T又可以理解成:
一个带
userName和age的对象类型。
4. 这个过程最想说明什么
它最想说明的是:
泛型函数不是让类型消失,而是让同一套规则在不同类型上都能保持成立。
六、很多时候你甚至不用手动写出泛型类型参数
这一点对纯小白特别友好。
因为很多人第一次看到泛型时,会以为每次调用都必须手写:
getFirstItem<number>([10,20,30]);其实很多时候并不需要。
1. 显式写出来的方式
constfirstNumber=getFirstItem<number>([10,20,30]);2. 更常见的推断方式
constfirstNumber=getFirstItem([10,20,30]);这里 TypeScript 往往会根据参数自动推断出T。
3. 当前阶段一个很实用的结论
你现在可以先记住:
泛型类型参数很多时候可以被推断出来,所以不必一看到泛型就觉得每次都要手写很多内容。
七、再看一个非常实用的泛型函数:把值放进数组
为了让泛型的感觉更稳一点,我们再看一个更短、更直观的例子。
functionwrapInArray<T>(value:T):T[]{return[value];}1. 这段代码在做什么
它的规则是:
不管你传进来什么类型,我都把它包成数组返回。
2. 传数字时
constnumberList=wrapInArray(100);你可以先理解成:
这次
T是number,所以返回值是number[]。
3. 传字符串时
constnameList=wrapInArray("小白");这次T就是string,返回值会更接近string[]。
4. 传对象时
constuserList=wrapInArray({userName:"小白",age:18});这次T就是对象类型,返回值是对象数组。
5. 这个例子为什么特别适合入门
因为它让你很容易看到泛型真正复用的是什么:
不是具体类型,而是“包装成数组”这条规则。
八、泛型和any看起来都很“灵活”,但本质完全不一样
这一节非常重要。
因为很多初学者第一次接触泛型时,很容易想:
“那我直接用
any不也能兼容很多类型吗?”
表面上看,好像有点像。
但本质差很多。
九、为什么说泛型不是any
先看一个any版本:
functiongetFirstItem(items:any[]):any{returnitems[0];}这段代码当然“能用”。
但问题在于:
类型信息丢了。
例如:
constfirstValue=getFirstItem(["HTML","CSS"]);这里你虽然知道自己传进去的是字符串数组,但函数返回的却只是:
any这意味着 TypeScript 基本帮不上你太多。
1. 再看泛型版本
functiongetFirstItem<T>(items:T[]):T|undefined{returnitems[0];}现在如果你传入:
constfirstCourse=getFirstItem(["HTML","CSS"]);TypeScript 就更有机会知道:
这里返回的是
string | undefined
2. 这就是核心差别
你可以先用一句很重要的话记住:
any是放弃类型信息,泛型是保留类型信息,但让类型延后确定。
3. 这也是为什么泛型更适合真实项目
因为真实项目里,我们通常并不是“不在乎类型”,而是:
希望这套规则能适配不同类型,同时又别把类型信息弄丢。
十、泛型到底在复用什么
这一点非常值得你停一下想清楚。
因为很多人一开始会把泛型误解成:
“泛型是在复用代码里的值。”
其实不是。
更准确地说:
泛型在复用的是“类型规则”。
1. 例如getFirstItem<T>
它复用的规则是:
- 输入是某种类型组成的数组
- 输出是这种类型的第一项
2. 例如wrapInArray<T>
它复用的规则是:
- 输入是某种类型的值
- 输出是这种类型组成的数组
3. 所以泛型最适合什么场景
最适合:
逻辑规则固定,但具体类型要根据使用场景变化的地方。
这句话几乎就是判断“值不值得上泛型”的最好入门标准。
十一、泛型在列表数据结构里为什么特别常见
走到这里,我们开始进入“常见实用场景”。
前端开发里,列表数据结构特别常见。
例如:
- 用户列表
- 商品列表
- 课程列表
- 待办事项列表
它们虽然内容不同,但有些外层结构往往很像。
例如很多列表数据都会长这样:
{total:100,items:[...]}也就是说:
外壳一样,只是
items里面装的类型不同。
这就非常适合泛型。
十二、一个最基础的泛型列表结构例子
你可以先定义这样一个类型别名:
typeListData<T>={total:number;items:T[];};1. 这段代码可以先怎么理解
你可以把它读成:
ListData是一个通用列表结构,它一定有total,也一定有items,但items里装什么类型,要等真正使用时再决定。
2. 传入商品类型时
先定义商品结构:
interfaceProductInfo{name:string;price:number;}再使用泛型列表结构:
typeListData<T>={total:number;items:T[];};constproductListData:ListData<ProductInfo>={total:2,items:[{name:"机械键盘",price:399},{name:"桌面音箱",price:299}]};3. 传入用户类型时
interfaceUserInfo{userName:string;age:number;}constuserListData:ListData<UserInfo>={total:2,items:[{userName:"小白",age:18},{userName:"小黑",age:20}]};4. 这个例子最想说明什么
它最想说明的是:
列表结构本身是一套可复用规则,而泛型让你能在复用规则的同时,保留里面每一项的真实类型信息。
十三、泛型在接口返回值结构里为什么也特别常见
除了列表,另一个非常高频的场景就是:
接口返回值结构
很多接口看起来虽然业务内容不同,但返回外层经常长得很像。
例如:
{code:200,message:"请求成功",data:...}这里真正变化的,往往是:
data里面装的具体内容。
所以这也很适合泛型。
十四、一个最基础的泛型接口返回值例子
你可以先定义这样一个接口:
interfaceApiResponse<T>{code:number;message:string;data:T;}1. 这段代码可以先怎么理解
你可以把它理解成:
这是一个通用接口返回值结构,它一定有
code、message、data,但data的具体类型要等使用时再决定。
2. 返回单个用户对象时
interfaceUserInfo{userName:string;age:number;}constuserResponse:ApiResponse<UserInfo>={code:200,message:"获取成功",data:{userName:"小白",age:18}};3. 返回商品数组时
interfaceProductInfo{name:string;price:number;}constproductResponse:ApiResponse<ProductInfo[]>={code:200,message:"获取成功",data:[{name:"机械键盘",price:399},{name:"桌面台灯",price:129}]};4. 为什么这个场景非常典型
因为真实前端项目里,接口返回值几乎总是在变:
- 有时返回单个对象
- 有时返回数组
- 有时返回分页结构
而泛型正好适合管理这种“外层规则稳定、内层类型变化”的情况。
十五、再看一个很贴近前端的泛型接口例子:下拉选项
前端里还有一种特别常见的数据结构:
选项结构
例如:
{label:"全部",value:"all"}有时value是字符串,有时却可能是数字。
这时候也特别适合泛型。
interfaceSelectOption<T>{label:string;value:T;}1. 字符串值的选项
constfilterOptions:SelectOption<string>[]=[{label:"全部",value:"all"},{label:"已完成",value:"completed"}];2. 数字值的选项
constscoreOptions:SelectOption<number>[]=[{label:"60 分",value:60},{label:"80 分",value:80}];3. 这个例子为什么很有帮助
因为它能让你更直观地看到:
泛型不是只存在于“算法函数”里,它也非常适合前端页面组件和 UI 数据结构。
十六、什么时候特别值得考虑使用泛型
这一节非常重要。
因为很多初学者一学到泛型,就容易进入另一个极端:
感觉哪里都想加一下。
这其实不太稳。
当前阶段更好的方式是:
先学会判断“什么时候它真的有价值”。
1. 逻辑规则固定,但具体类型会变化
例如:
- 获取数组首项
- 包装成数组
- 列表结构
- 接口返回值结构
2. 同一套类型结构会重复使用在多个不同业务数据上
例如:
ListData<T>ApiResponse<T>SelectOption<T>
3. 你想保留类型信息,而不是退回any
这也是泛型特别重要的一点。
如果你既想通用,又不想丢掉类型信息,泛型通常就是很自然的选择。
十七、什么时候不必硬上泛型
这一节和上一节一样重要。
因为 TypeScript 学习里最稳的状态,不是“见什么都想加泛型”,而是:
知道什么时候它值,什么时候不值。
1. 规则根本不通用时
例如某个函数就是只处理价格:
functionformatPrice(price:number):string{return`¥${price}`;}这种场景就完全没必要为了“显得高级”强行写成泛型。
2. 写成泛型反而更难懂时
如果你发现某段类型:
- 不用泛型时很清楚
- 一换成泛型就更绕
那当前阶段更推荐:
先保留更直白的写法。
3. 业务意义本身还没想清楚时
如果你连:
- 这个结构到底是不是通用的
- 它到底在哪些地方会复用
都还没想清楚,那通常也不适合急着上泛型。
十八、一个完整示例:把重复的数据处理逻辑抽成泛型版本
下面我们来做一个综合例子。
目标是:
把“从数组里取第一项”的重复逻辑,抽成一个真正可复用的泛型函数。
1. 先看重复写法
functiongetFirstUserName(items:string[]):string|undefined{returnitems[0];}functiongetFirstScore(items:number[]):number|undefined{returnitems[0];}functiongetFirstTodoTitle(items:{title:string}[]):{title:string}|undefined{returnitems[0];}这里真正重复的是:
“取数组第一项”这条规则。
2. 改成泛型版本
functiongetFirstItem<T>(items:T[]):T|undefined{returnitems[0];}3. 然后把它用于不同数据
constfirstCourse=getFirstItem(["HTML","CSS","JavaScript"]);constfirstScore=getFirstItem([90,88,100]);constfirstTodoItem=getFirstItem([{title:"学习 TypeScript"},{title:"完成泛型练习"}]);4. 这个例子最想让你建立什么感觉
不是“泛型很神奇”,而是:
泛型的价值,在于把同一套规则真正抽象成一份能重复用、同时又不丢类型信息的定义。
十九、初学者最容易踩的坑
这一章的坑很典型,而且很多都和“把泛型想得太神秘”或者“把泛型想得太万能”有关。
1. 一看到<T>就下意识害怕
其实它本质上只是:
一个类型占位符。
当前阶段不要把它想成复杂魔法。
2. 把泛型当成any的高级写法
这很危险。
要始终记住:
泛型是在保留类型信息,
any是在放弃类型信息。
3. 只看到“通用”,没看到“规则”
有些人会觉得泛型就是“哪里都能通用”。
其实不是。
更准确的理解应该是:
泛型适合“规则固定、类型变化”的地方。
4. 一上来就想写很复杂的泛型
现在不是这个阶段。
当前阶段你最该优先掌握的,是:
- 泛型函数
- 泛型列表结构
- 泛型接口返回值结构
先把这些最常用、最直观的场景用顺,就已经很好了。
5. 写了泛型,但业务名字很模糊
例如:
typeData<T>={total:number;items:T[];};语法虽然没错,但业务语义比较弱。
很多时候更清楚的名字会更好,例如:
typeListData<T>={total:number;items:T[];};6. 泛型写得比不用还难懂
这是一个非常实际的提醒。
如果某段泛型代码让你自己看了都更晕,那就说明:
当前这层抽象可能过度了。
别急着追求复杂,先保证清楚。
二十、本章实践练习
这一章特别适合做“从重复写法走向通用写法”的练习。
1. 练习 1:写一个返回数组首项的泛型函数
请你自己写出类似下面这个函数:
functiongetFirstItem<T>(items:T[]):T|undefined{returnitems[0];}然后分别传入:
- 字符串数组
- 数字数组
- 对象数组
这个练习重点是帮助你真正体会:
同一套规则,可以在不同类型上复用。
2. 练习 2:为通用列表数据结构加上泛型
请你自己定义一个类似:
typeListData<T>={total:number;items:T[];};的结构。
然后分别试着把它用于:
- 用户列表
- 商品列表
这个练习重点是训练你:
识别“外壳固定、内部类型变化”的结构。
3. 练习 3:为接口返回值结构加上泛型
请你自己设计一个:
interfaceApiResponse<T>{code:number;message:string;data:T;}这样的接口结构。
然后尝试用于:
- 返回单个用户对象
- 返回商品数组
这个练习重点是帮助你建立:
泛型在前端接口数据里非常常见。
4. 练习 4:把一个重复的数据处理函数抽象成泛型版本
请你自己找一个前面写过的“几乎同逻辑、只换了类型”的函数。
例如:
- 返回数组最后一项
- 包装成数组
- 输出长度信息
然后尝试把它改成泛型版本。
这个练习重点不是追求多复杂,而是训练你去发现:
哪些地方真的存在“规则复用”的机会。
二十一、学习重点提示
这一章最重要的,不是立刻把泛型写得很复杂,而是先真正建立“泛型到底在解决什么问题”的感觉。
请重点记住:
- 泛型的核心,不是让类型变模糊,而是让同一套规则适配不同类型。
<T>本质上只是一个类型占位符,不是什么神秘符号。- 泛型和
any完全不是一回事,泛型是在保留类型信息。 - 规则固定、类型变化的场景,特别适合考虑泛型。
- 列表结构和接口返回值结构,是前端里最常见的泛型使用场景之一。
- 如果一个泛型写法看起来比不用泛型还难懂,那通常应该先简化。
- 泛型的重点是“复用同一套规则”,不是追求复杂写法。
如果你只记一句话,请记住:
泛型的真正价值,是让你在不丢类型信息的前提下,把可重复的类型规则抽成一份通用定义。
二十二、本章小结
这一章,我们把 TypeScript 又往前推进了一步。
你已经学习了:
- 为什么会需要泛型
- 泛型最基础的写法是什么
<T>到底代表什么- 泛型和
any的区别 - 泛型函数如何工作
- 泛型在列表结构和接口返回值结构里的常见用法
- 什么时候该用泛型,什么时候没必要硬上
更重要的是,你已经开始建立一种很关键的抽象意识:
当多段代码的规则完全一样,只是具体类型不同,就应该开始考虑能不能把“类型规则本身”抽出来复用。
这一步非常关键。
因为从这里开始,TypeScript 对你来说不再只是“写清楚一个个具体类型”,而开始变成:
组织和复用类型规则。
二十三、课后思考题
请你思考下面几个问题:
- 为什么说泛型最适合“规则固定、类型变化”的场景?
- 为什么泛型和
any虽然都看起来很灵活,但本质完全不同? - 为什么列表结构和接口返回值结构特别适合用泛型?
<T>到底在表达什么?为什么说它不是魔法,而只是类型占位符?- 为什么泛型的重点不是“写得更复杂”,而是“让规则更可复用”?
- 回头看你自己写过的 TypeScript 代码,哪些地方已经开始出现“逻辑一样、类型不同”的重复了?
建议你用自己的话把答案写下来。只要你能把这些问题说清楚,说明你已经真正开始掌握 TypeScript 里“复用类型规则”的第一层能力了。
二十四、下一章预告
下一章我们会继续学习:
从零开始学前端 | 第二十五章:在前端项目中使用 TypeScript
到那时,你会开始真正把前面学过的基础类型、接口、类型别名、泛型带进一个真实的前端项目环境里,看看:
TypeScript 在编辑器提示、报错修复、项目迁移和实际开发流程里,到底是怎么工作的。