从零开始学前端 | 第二十四章:泛型与常见实用场景

📅 2026/7/21 9:50:59 👁️ 阅读次数 📝 编程学习
从零开始学前端 | 第二十四章:泛型与常见实用场景

本章定位

上一章,我们已经把 TypeScript 里“给类型起名字”这件事真正建立起来了。

你已经学习了:

  1. interface可以描述稳定的对象结构。
  2. type可以给对象、联合类型、函数类型起名字。
  3. 可选属性?可以表达“这个字段可以没有”。
  4. 函数类型本质上是在表达“它吃什么、吐什么”。

也就是说,到现在为止,你已经能写出这样的代码:

interfaceUserInfo{userName:string;age:number;}typeSubmitHandler=(value:string)=>void;

这一步非常关键。

因为你已经不只是会给变量后面加类型,而是开始会组织“类型结构”了。

但如果你继续往前写,很快又会遇到一个新问题:

有些规则本身是通用的,只是里面装的具体类型不同。

例如:

  • “获取数组第一项”这件事,对字符串数组和数字数组都成立
  • “列表数据结构”这件事,对商品列表和用户列表都成立
  • “接口返回值结构”这件事,对用户数据和商品数据都成立

如果你每次都针对不同类型重新写一份,就会越来越重复。

例如你可能会写出这样的代码:

functiongetFirstNumber(items:number[]):number|undefined{returnitems[0];}functiongetFirstString(items:string[]):string|undefined{returnitems[0];}

这里逻辑几乎一样,差别只是:

具体类型不同。

这时候就需要一种能力,帮你表达:

这套规则是通用的,但里面的具体类型可以后面再决定。

这就是泛型最核心的作用。

这一章要解决的核心问题就是:

当同一套类型规则需要在不同数据上重复使用时,TypeScript 该怎样既保留类型信息,又避免重复书写。

本章学习目标

学完这一章后,你应该能做到:

  1. 理解什么是泛型,以及它到底在解决什么问题。
  2. 掌握泛型函数的最基础写法。
  3. 理解尖括号里的T到底代表什么。
  4. 理解泛型和any的本质区别。
  5. 学会在列表数据结构里使用泛型。
  6. 学会在接口返回值结构里使用泛型。
  7. 知道什么时候值得用泛型,什么时候没必要硬上。
  8. 避开这一阶段最常见的泛型入门坑。

一、为什么会需要“同一套规则,适配不同类型”

先看一个非常直观的问题。

假设你要写一个函数:

返回数组的第一项。

如果你先只考虑数字数组,可能会这样写:

functiongetFirstNumber(items:number[]):number|undefined{returnitems[0];}

如果你又想支持字符串数组,可能会接着写:

functiongetFirstString(items:string[]):string|undefined{returnitems[0];}

如果以后还要支持布尔数组呢?

functiongetFirstBoolean(items:boolean[]):boolean|undefined{returnitems[0];}

1. 这里真正重复的是什么

不是数据,而是:

规则完全一样。

它们本质都在做:

  1. 接收一个数组
  2. 返回第 1 项
  3. 如果没有,就可能是undefined

唯一变化的,只是数组里装的类型不同。

2. 如果继续这样写,会有什么问题

最直观的问题有三个:

  1. 重复代码越来越多
  2. 逻辑明明一样,却要维护很多份
  3. 每新增一种类型,都要再写一份函数

3. 这时候最自然的需求是什么

就是:

能不能把“返回数组第一项”这套规则只写一份,但让它适配不同类型?

这正是泛型登场的地方。

二、泛型到底是什么

“泛型”这个词第一次听起来,很多人会觉得有点抽象。

你现在可以先把它理解成一句非常直白的话:

先把规则写出来,但里面的具体类型先不写死,等真正使用的时候再决定。

这句话非常重要。

它几乎就是泛型的入门理解核心。

1. 泛型不是“不要类型”

这一点特别关键。

泛型并不是说:

这个地方我懒得管类型了,随便来吧。

恰恰相反,泛型是在表达:

这个地方我很在乎类型,只是这个类型现在还不能提前写死。

2. 泛型不是让规则更模糊,而是让规则更可复用

这也是很多初学者最容易一下子没转过来的地方。

你要慢慢建立的理解是:

泛型并不是放弃规则,而是让同一套规则能够作用在不同类型上。

三、先从最基础的泛型函数开始看

回到刚才“返回数组第一项”的例子。

有了泛型之后,它可以写成这样:

functiongetFirstItem<T>(items:T[]):T|undefined{returnitems[0];}

第一次看到这段代码时,很多人会卡在两个地方:

  1. <T>是什么
  2. 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;}

这里的ab是“值变量”的名字。

而在泛型里:

<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 通常会根据你传入的数组内容,推断出:

这次的Tnumber

所以firstNumber的类型就会更接近:

number|undefined

2. 传入字符串数组

constfirstCourse=getFirstItem(["HTML","CSS","JavaScript"]);

这时候 TypeScript 又会推断出:

这次的Tstring

所以返回值类型就会更接近:

string|undefined

3. 传入对象数组

constfirstUser=getFirstItem([{userName:"小白",age:18},{userName:"小黑",age:20}]);

这时候T又可以理解成:

一个带userNameage的对象类型。

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);

你可以先理解成:

这次Tnumber,所以返回值是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>

它复用的规则是:

  1. 输入是某种类型组成的数组
  2. 输出是这种类型的第一项

2. 例如wrapInArray<T>

它复用的规则是:

  1. 输入是某种类型的值
  2. 输出是这种类型组成的数组

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. 这段代码可以先怎么理解

你可以把它理解成:

这是一个通用接口返回值结构,它一定有codemessagedata,但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];}

然后分别传入:

  1. 字符串数组
  2. 数字数组
  3. 对象数组

这个练习重点是帮助你真正体会:

同一套规则,可以在不同类型上复用。

2. 练习 2:为通用列表数据结构加上泛型

请你自己定义一个类似:

typeListData<T>={total:number;items:T[];};

的结构。

然后分别试着把它用于:

  1. 用户列表
  2. 商品列表

这个练习重点是训练你:

识别“外壳固定、内部类型变化”的结构。

3. 练习 3:为接口返回值结构加上泛型

请你自己设计一个:

interfaceApiResponse<T>{code:number;message:string;data:T;}

这样的接口结构。

然后尝试用于:

  1. 返回单个用户对象
  2. 返回商品数组

这个练习重点是帮助你建立:

泛型在前端接口数据里非常常见。

4. 练习 4:把一个重复的数据处理函数抽象成泛型版本

请你自己找一个前面写过的“几乎同逻辑、只换了类型”的函数。

例如:

  • 返回数组最后一项
  • 包装成数组
  • 输出长度信息

然后尝试把它改成泛型版本。

这个练习重点不是追求多复杂,而是训练你去发现:

哪些地方真的存在“规则复用”的机会。

二十一、学习重点提示

这一章最重要的,不是立刻把泛型写得很复杂,而是先真正建立“泛型到底在解决什么问题”的感觉。

请重点记住:

  1. 泛型的核心,不是让类型变模糊,而是让同一套规则适配不同类型。
  2. <T>本质上只是一个类型占位符,不是什么神秘符号。
  3. 泛型和any完全不是一回事,泛型是在保留类型信息。
  4. 规则固定、类型变化的场景,特别适合考虑泛型。
  5. 列表结构和接口返回值结构,是前端里最常见的泛型使用场景之一。
  6. 如果一个泛型写法看起来比不用泛型还难懂,那通常应该先简化。
  7. 泛型的重点是“复用同一套规则”,不是追求复杂写法。

如果你只记一句话,请记住:

泛型的真正价值,是让你在不丢类型信息的前提下,把可重复的类型规则抽成一份通用定义。

二十二、本章小结

这一章,我们把 TypeScript 又往前推进了一步。

你已经学习了:

  • 为什么会需要泛型
  • 泛型最基础的写法是什么
  • <T>到底代表什么
  • 泛型和any的区别
  • 泛型函数如何工作
  • 泛型在列表结构和接口返回值结构里的常见用法
  • 什么时候该用泛型,什么时候没必要硬上

更重要的是,你已经开始建立一种很关键的抽象意识:

当多段代码的规则完全一样,只是具体类型不同,就应该开始考虑能不能把“类型规则本身”抽出来复用。

这一步非常关键。

因为从这里开始,TypeScript 对你来说不再只是“写清楚一个个具体类型”,而开始变成:

组织和复用类型规则。

二十三、课后思考题

请你思考下面几个问题:

  1. 为什么说泛型最适合“规则固定、类型变化”的场景?
  2. 为什么泛型和any虽然都看起来很灵活,但本质完全不同?
  3. 为什么列表结构和接口返回值结构特别适合用泛型?
  4. <T>到底在表达什么?为什么说它不是魔法,而只是类型占位符?
  5. 为什么泛型的重点不是“写得更复杂”,而是“让规则更可复用”?
  6. 回头看你自己写过的 TypeScript 代码,哪些地方已经开始出现“逻辑一样、类型不同”的重复了?

建议你用自己的话把答案写下来。只要你能把这些问题说清楚,说明你已经真正开始掌握 TypeScript 里“复用类型规则”的第一层能力了。

二十四、下一章预告

下一章我们会继续学习:

从零开始学前端 | 第二十五章:在前端项目中使用 TypeScript

到那时,你会开始真正把前面学过的基础类型、接口、类型别名、泛型带进一个真实的前端项目环境里,看看:

TypeScript 在编辑器提示、报错修复、项目迁移和实际开发流程里,到底是怎么工作的。