从零开始学前端 | 第二十四章:泛型与常见实用场景
本章定位上一章我们已经把 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. 泛型不是让规则更模糊而是让规则更可复用这也是很多初学者最容易一下子没转过来的地方。你要慢慢建立的理解是泛型并不是放弃规则而是让同一套规则能够作用在不同类型上。三、先从最基础的泛型函数开始看回到刚才“返回数组第一项”的例子。有了泛型之后它可以写成这样functiongetFirstItemT(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){returnab;}这里的a、b是“值变量”的名字。而在泛型里T这个T则可以先理解成“类型变量”的名字。2. 为什么很多教程都写T因为这是一种非常常见的约定写法。T可以理解成Type 的首字母。3.T不是唯一写法例如你也可能会看到ItemType或者ValueType这些都可以。当前阶段你先记住T只是最常见的泛型类型占位符不是什么固定含义的魔法字母。五、泛型函数怎么真正用起来现在你已经知道泛型函数长什么样了。接下来看看它怎么用。1. 传入数字数组functiongetFirstItemT(items:T[]):T|undefined{returnitems[0];}constfirstNumbergetFirstItem([10,20,30]);这里 TypeScript 通常会根据你传入的数组内容推断出这次的T是number所以firstNumber的类型就会更接近number|undefined2. 传入字符串数组constfirstCoursegetFirstItem([HTML,CSS,JavaScript]);这时候 TypeScript 又会推断出这次的T是string所以返回值类型就会更接近string|undefined3. 传入对象数组constfirstUsergetFirstItem([{userName:小白,age:18},{userName:小黑,age:20}]);这时候T又可以理解成一个带userName和age的对象类型。4. 这个过程最想说明什么它最想说明的是泛型函数不是让类型消失而是让同一套规则在不同类型上都能保持成立。六、很多时候你甚至不用手动写出泛型类型参数这一点对纯小白特别友好。因为很多人第一次看到泛型时会以为每次调用都必须手写getFirstItemnumber([10,20,30]);其实很多时候并不需要。1. 显式写出来的方式constfirstNumbergetFirstItemnumber([10,20,30]);2. 更常见的推断方式constfirstNumbergetFirstItem([10,20,30]);这里 TypeScript 往往会根据参数自动推断出T。3. 当前阶段一个很实用的结论你现在可以先记住泛型类型参数很多时候可以被推断出来所以不必一看到泛型就觉得每次都要手写很多内容。七、再看一个非常实用的泛型函数把值放进数组为了让泛型的感觉更稳一点我们再看一个更短、更直观的例子。functionwrapInArrayT(value:T):T[]{return[value];}1. 这段代码在做什么它的规则是不管你传进来什么类型我都把它包成数组返回。2. 传数字时constnumberListwrapInArray(100);你可以先理解成这次T是number所以返回值是number[]。3. 传字符串时constnameListwrapInArray(小白);这次T就是string返回值会更接近string[]。4. 传对象时constuserListwrapInArray({userName:小白,age:18});这次T就是对象类型返回值是对象数组。5. 这个例子为什么特别适合入门因为它让你很容易看到泛型真正复用的是什么不是具体类型而是“包装成数组”这条规则。八、泛型和any看起来都很“灵活”但本质完全不一样这一节非常重要。因为很多初学者第一次接触泛型时很容易想“那我直接用any不也能兼容很多类型吗”表面上看好像有点像。但本质差很多。九、为什么说泛型不是any先看一个any版本functiongetFirstItem(items:any[]):any{returnitems[0];}这段代码当然“能用”。但问题在于类型信息丢了。例如constfirstValuegetFirstItem([HTML,CSS]);这里你虽然知道自己传进去的是字符串数组但函数返回的却只是any这意味着 TypeScript 基本帮不上你太多。1. 再看泛型版本functiongetFirstItemT(items:T[]):T|undefined{returnitems[0];}现在如果你传入constfirstCoursegetFirstItem([HTML,CSS]);TypeScript 就更有机会知道这里返回的是string | undefined2. 这就是核心差别你可以先用一句很重要的话记住any是放弃类型信息泛型是保留类型信息但让类型延后确定。3. 这也是为什么泛型更适合真实项目因为真实项目里我们通常并不是“不在乎类型”而是希望这套规则能适配不同类型同时又别把类型信息弄丢。十、泛型到底在复用什么这一点非常值得你停一下想清楚。因为很多人一开始会把泛型误解成“泛型是在复用代码里的值。”其实不是。更准确地说泛型在复用的是“类型规则”。1. 例如getFirstItemT它复用的规则是输入是某种类型组成的数组输出是这种类型的第一项2. 例如wrapInArrayT它复用的规则是输入是某种类型的值输出是这种类型组成的数组3. 所以泛型最适合什么场景最适合逻辑规则固定但具体类型要根据使用场景变化的地方。这句话几乎就是判断“值不值得上泛型”的最好入门标准。十一、泛型在列表数据结构里为什么特别常见走到这里我们开始进入“常见实用场景”。前端开发里列表数据结构特别常见。例如用户列表商品列表课程列表待办事项列表它们虽然内容不同但有些外层结构往往很像。例如很多列表数据都会长这样{total:100,items:[...]}也就是说外壳一样只是items里面装的类型不同。这就非常适合泛型。十二、一个最基础的泛型列表结构例子你可以先定义这样一个类型别名typeListDataT{total:number;items:T[];};1. 这段代码可以先怎么理解你可以把它读成ListData是一个通用列表结构它一定有total也一定有items但items里装什么类型要等真正使用时再决定。2. 传入商品类型时先定义商品结构interfaceProductInfo{name:string;price:number;}再使用泛型列表结构typeListDataT{total:number;items:T[];};constproductListData:ListDataProductInfo{total:2,items:[{name:机械键盘,price:399},{name:桌面音箱,price:299}]};3. 传入用户类型时interfaceUserInfo{userName:string;age:number;}constuserListData:ListDataUserInfo{total:2,items:[{userName:小白,age:18},{userName:小黑,age:20}]};4. 这个例子最想说明什么它最想说明的是列表结构本身是一套可复用规则而泛型让你能在复用规则的同时保留里面每一项的真实类型信息。十三、泛型在接口返回值结构里为什么也特别常见除了列表另一个非常高频的场景就是接口返回值结构很多接口看起来虽然业务内容不同但返回外层经常长得很像。例如{code:200,message:请求成功,data:...}这里真正变化的往往是data里面装的具体内容。所以这也很适合泛型。十四、一个最基础的泛型接口返回值例子你可以先定义这样一个接口interfaceApiResponseT{code:number;message:string;data:T;}1. 这段代码可以先怎么理解你可以把它理解成这是一个通用接口返回值结构它一定有code、message、data但data的具体类型要等使用时再决定。2. 返回单个用户对象时interfaceUserInfo{userName:string;age:number;}constuserResponse:ApiResponseUserInfo{code:200,message:获取成功,data:{userName:小白,age:18}};3. 返回商品数组时interfaceProductInfo{name:string;price:number;}constproductResponse:ApiResponseProductInfo[]{code:200,message:获取成功,data:[{name:机械键盘,price:399},{name:桌面台灯,price:129}]};4. 为什么这个场景非常典型因为真实前端项目里接口返回值几乎总是在变有时返回单个对象有时返回数组有时返回分页结构而泛型正好适合管理这种“外层规则稳定、内层类型变化”的情况。十五、再看一个很贴近前端的泛型接口例子下拉选项前端里还有一种特别常见的数据结构选项结构例如{label:全部,value:all}有时value是字符串有时却可能是数字。这时候也特别适合泛型。interfaceSelectOptionT{label:string;value:T;}1. 字符串值的选项constfilterOptions:SelectOptionstring[][{label:全部,value:all},{label:已完成,value:completed}];2. 数字值的选项constscoreOptions:SelectOptionnumber[][{label:60 分,value:60},{label:80 分,value:80}];3. 这个例子为什么很有帮助因为它能让你更直观地看到泛型不是只存在于“算法函数”里它也非常适合前端页面组件和 UI 数据结构。十六、什么时候特别值得考虑使用泛型这一节非常重要。因为很多初学者一学到泛型就容易进入另一个极端感觉哪里都想加一下。这其实不太稳。当前阶段更好的方式是先学会判断“什么时候它真的有价值”。1. 逻辑规则固定但具体类型会变化例如获取数组首项包装成数组列表结构接口返回值结构2. 同一套类型结构会重复使用在多个不同业务数据上例如ListDataTApiResponseTSelectOptionT3. 你想保留类型信息而不是退回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. 改成泛型版本functiongetFirstItemT(items:T[]):T|undefined{returnitems[0];}3. 然后把它用于不同数据constfirstCoursegetFirstItem([HTML,CSS,JavaScript]);constfirstScoregetFirstItem([90,88,100]);constfirstTodoItemgetFirstItem([{title:学习 TypeScript},{title:完成泛型练习}]);4. 这个例子最想让你建立什么感觉不是“泛型很神奇”而是泛型的价值在于把同一套规则真正抽象成一份能重复用、同时又不丢类型信息的定义。十九、初学者最容易踩的坑这一章的坑很典型而且很多都和“把泛型想得太神秘”或者“把泛型想得太万能”有关。1. 一看到T就下意识害怕其实它本质上只是一个类型占位符。当前阶段不要把它想成复杂魔法。2. 把泛型当成any的高级写法这很危险。要始终记住泛型是在保留类型信息any是在放弃类型信息。3. 只看到“通用”没看到“规则”有些人会觉得泛型就是“哪里都能通用”。其实不是。更准确的理解应该是泛型适合“规则固定、类型变化”的地方。4. 一上来就想写很复杂的泛型现在不是这个阶段。当前阶段你最该优先掌握的是泛型函数泛型列表结构泛型接口返回值结构先把这些最常用、最直观的场景用顺就已经很好了。5. 写了泛型但业务名字很模糊例如typeDataT{total:number;items:T[];};语法虽然没错但业务语义比较弱。很多时候更清楚的名字会更好例如typeListDataT{total:number;items:T[];};6. 泛型写得比不用还难懂这是一个非常实际的提醒。如果某段泛型代码让你自己看了都更晕那就说明当前这层抽象可能过度了。别急着追求复杂先保证清楚。二十、本章实践练习这一章特别适合做“从重复写法走向通用写法”的练习。1. 练习 1写一个返回数组首项的泛型函数请你自己写出类似下面这个函数functiongetFirstItemT(items:T[]):T|undefined{returnitems[0];}然后分别传入字符串数组数字数组对象数组这个练习重点是帮助你真正体会同一套规则可以在不同类型上复用。2. 练习 2为通用列表数据结构加上泛型请你自己定义一个类似typeListDataT{total:number;items:T[];};的结构。然后分别试着把它用于用户列表商品列表这个练习重点是训练你识别“外壳固定、内部类型变化”的结构。3. 练习 3为接口返回值结构加上泛型请你自己设计一个interfaceApiResponseT{code:number;message:string;data:T;}这样的接口结构。然后尝试用于返回单个用户对象返回商品数组这个练习重点是帮助你建立泛型在前端接口数据里非常常见。4. 练习 4把一个重复的数据处理函数抽象成泛型版本请你自己找一个前面写过的“几乎同逻辑、只换了类型”的函数。例如返回数组最后一项包装成数组输出长度信息然后尝试把它改成泛型版本。这个练习重点不是追求多复杂而是训练你去发现哪些地方真的存在“规则复用”的机会。二十一、学习重点提示这一章最重要的不是立刻把泛型写得很复杂而是先真正建立“泛型到底在解决什么问题”的感觉。请重点记住泛型的核心不是让类型变模糊而是让同一套规则适配不同类型。T本质上只是一个类型占位符不是什么神秘符号。泛型和any完全不是一回事泛型是在保留类型信息。规则固定、类型变化的场景特别适合考虑泛型。列表结构和接口返回值结构是前端里最常见的泛型使用场景之一。如果一个泛型写法看起来比不用泛型还难懂那通常应该先简化。泛型的重点是“复用同一套规则”不是追求复杂写法。如果你只记一句话请记住泛型的真正价值是让你在不丢类型信息的前提下把可重复的类型规则抽成一份通用定义。二十二、本章小结这一章我们把 TypeScript 又往前推进了一步。你已经学习了为什么会需要泛型泛型最基础的写法是什么T到底代表什么泛型和any的区别泛型函数如何工作泛型在列表结构和接口返回值结构里的常见用法什么时候该用泛型什么时候没必要硬上更重要的是你已经开始建立一种很关键的抽象意识当多段代码的规则完全一样只是具体类型不同就应该开始考虑能不能把“类型规则本身”抽出来复用。这一步非常关键。因为从这里开始TypeScript 对你来说不再只是“写清楚一个个具体类型”而开始变成组织和复用类型规则。二十三、课后思考题请你思考下面几个问题为什么说泛型最适合“规则固定、类型变化”的场景为什么泛型和any虽然都看起来很灵活但本质完全不同为什么列表结构和接口返回值结构特别适合用泛型T到底在表达什么为什么说它不是魔法而只是类型占位符为什么泛型的重点不是“写得更复杂”而是“让规则更可复用”回头看你自己写过的 TypeScript 代码哪些地方已经开始出现“逻辑一样、类型不同”的重复了建议你用自己的话把答案写下来。只要你能把这些问题说清楚说明你已经真正开始掌握 TypeScript 里“复用类型规则”的第一层能力了。二十四、下一章预告下一章我们会继续学习从零开始学前端 | 第二十五章在前端项目中使用 TypeScript到那时你会开始真正把前面学过的基础类型、接口、类型别名、泛型带进一个真实的前端项目环境里看看TypeScript 在编辑器提示、报错修复、项目迁移和实际开发流程里到底是怎么工作的。