同学们好,我是 Eugene(尤金),一个拥有多年中后台开发经验的前端工程师~
(Eugene 发音很简单,/juːˈdʒiːn/,大家怎么顺口怎么叫就好)
你是否也有过:明明学过很多技术,一到关键时候却讲不出来、甚至写不出来?
你是否也曾怀疑自己,是不是太笨了,明明感觉会,却总差一口气?
就算想沉下心从头梳理,可工作那么忙,回家还要陪伴家人。
一天只有 24 小时,时间永远不够用,常常感到力不从心。
技术行业,本就是逆水行舟,不进则退。
如果你也有同样的困扰,别慌。
从现在开始,跟着我一起心态归零,利用碎片时间,来一次彻彻底底的基础扫盲。
这一次,我们一起慢慢来,扎扎实实变强。
不搞花里胡哨的理论堆砌,只分享看得懂、用得上的前端干货,
咱们一起稳步积累,真正摆脱“面向搜索引擎写代码”的尴尬。
开篇
从「完全不懂泛型」一路走到「看懂下面这段代码到底在干嘛」:
// 此代码为 VxeTable 组件库 Grid 配置式表格数据分页示例代码部分片段
<script lang="ts" setup>
import { reactive } from 'vue'
import type { VxeGridProps, VxeGridListeners } from 'vxe-table'
interface RowVO {
id: number
name: string
role: string
sex: string
age: number
address: string
}
const gridOptions = reactive<VxeGridProps<RowVO>>({
showOverflow: true,
border: true,
loading: false,
height: 500,
pagerConfig: pagerVO,
columns: [
{ type: 'seq', width: 70, fixed: 'left' },
{ field: 'name', title: 'Name', minWidth: 160 },
{ field: 'email', title: 'Email', minWidth: 160 },
{ field: 'nickname', title: 'Nickname', minWidth: 160 },
{ field: 'age', title: 'Age', width: 100 },
{ field: 'role', title: 'Role', minWidth: 160 },
{ field: 'amount', title: 'Amount', width: 140 },
{ field: 'updateDate', title: 'Update Date', visible: false },
{ field: 'createDate', title: 'Create Date', visible: false },
],
data: [],
})
</script>
VxeTable 组件库简介:
- 由于这篇文章引用到了 VxeTable 组件库的代码,所以在这里给没接触过的小伙伴做一个简单的介绍,老司机可自行跳过。
- VxeTable 是一个基于 Vue 的表格组件库,提供表格、表单、工具栏、分页等组件,适合中后台场景。性能与功能都较强,但学习成本和按需引入的配置需要投入时间。如果你的项目以表格为核心,且需要虚拟滚动、复杂交互等功能,VxeTable 是合适的选择。感兴趣的小伙伴可以通过下方贴上的官网链接学习了解。
(PS:即使没用过 VxeTable 也不影响你看懂这篇文章)
官网链接:VxeTable 官网
一、什么是泛型?一句话版本
TypeScript 泛型 = 给「类型」加参数。
- 函数可以有参数:
function fn(x: number) {} - 类型也可以有「参数」:
Array<string>
这里 Array 就是一个「带类型参数」的类型,<string> 就是「传给它的类型参数」。
用人话说:
泛型就是:我写一份通用的类型 / 函数,真正用的时候再告诉它具体用什么类型。
二、最普通的一层泛型
1. 最熟悉的例子:数组
// 这俩是完全等价的
const list1: string[] = []
const list2: Array<string> = []
Array<T>是一个泛型类型T是它的类型参数Array<string>表示「元素类型是string的数组」
2. 自己写一个泛型函数
function identity<T>(value: T): T {
return value
}
identity<number>(1) // T 被替换成 number
identity<string>('hi') // T 被替换成 string
你可以理解为:
- 定义:
identity<T>→T是一个「占位的类型」 - 使用:
identity<number>→ 这次调用里「把T换成number」
或许有同学不理解为什么要在函数名称后面写 <T>。不用纠结,这是固定的写法,就像你要使用变量,就要先声明一样,如:
let data = []
data.push(123)
如果此处没有声明 data,便用不了 data。同理,如果不在函数名称后面写 <T> 声明一下这是泛型参数,TypeScript 无法识别 T 是什么,如下:
// 错误:找不到名称 'T'
function identity(value: T): T {
return value
}
// 报错:Cannot find name 'T'
TypeScript 会把 T 当作一个未声明的类型,因此报错。
三、类型也可以是泛型:接口 / type
1. 泛型接口
// 使用时传入不同的 T
interface ApiResponse<T> {
code: number
msg: string
data: T
}
interface User {
id: number
name: string
}
const res1: ApiResponse<User> = {
code: 0,
msg: 'ok',
data: { id: 1, name: '张三' },
}
const res2: ApiResponse<string[]> = {
code: 0,
msg: 'ok',
data: ['a', 'b'],
}
观察:
ApiResponse<T>自己并不知道T是啥- 真正用的时候写
ApiResponse<User>/ApiResponse<string[]> - TypeScript 在这一刻才把
T替换掉
四、嵌套泛型:泛型里面再套泛型
其实很简单,就是「类型参数本身也是一个泛型类型」。
// 一层:数组里放字符串
Array<string>
// 两层:Promise 里放数组,数组里放字符串
Promise<Array<string>>
// 换个写法更直观
type StringArray = Array<string>
type StringArrayPromise = Promise<StringArray>
你可以这么想:
- 第一层:
Array<T> - 第二层:
Promise<第一层>
五、回到文章最开始的例子:VxeGridProps<RowVO>
先看定义的行数据类型:
interface RowVO {
id: number
name: string
role: string
sex: string
age: number
address: string
}
然后:
const gridOptions = reactive<VxeGridProps<RowVO>>({...})
拆开理解:
VxeGridProps<D = any>是 vxe-table 提供的泛型接口- 你写的是
VxeGridProps<RowVO> - 这一刻,
D就被替换成了RowVO
也就是在这一整次使用里,可以把它脑补成:
// 伪代码,仅用于理解
interface VxeGridProps_RowVO extends VxeTableProps<RowVO> {
columns?: VxeGridPropTypes.Columns<RowVO>
proxyConfig?: VxeGridPropTypes.ProxyConfig<RowVO>
// ...
}
可能很多同学看到这里会感到些疑惑,怎么一会儿 T 一会儿 D 的。其实不管是 T 还是 D 都是类型变量的自定义名称,叫什么都无所谓,语法上没有任何固定含义,就像你写 JS 时给变量起名 num / name / age 一样,只是前端社区形成了「约定俗成的命名习惯」,用不同字母对应不同语义,让代码更易读。
| 字母 | 全称 | 含义/使用场景 | 例子 |
|---|---|---|---|
| T | Type | 通用类型(最常用,无特殊语义时都用 T) | first<T>(arr: T[]) |
| D | Default/Date | 通常指「默认类型」或「日期类型」(小众) | 泛型接口里的默认类型:interface Config<D = string> |
| K | Key | 表示对象的「键」类型 | getKey<K extends string>(obj: { [k: K]: any }, key: K) |
| V | Value | 表示对象的「值」类型 | Map<K, V>(TS 内置的 Map 泛型) |
| E | Element | 表示数组/集合的「元素」类型 | Array<E>(TS 内置的数组泛型) |
| P | Parameter | 表示函数的「参数」类型 | function wrap<P>(fn: (arg: P) => void, arg: P) |
六、类型参数是怎么一层一层「传下去」的?
到这一步,为了更好的理解泛型,我将带着同学们追溯源码。一起来追踪一下 VxeTable 的泛型实现吧。
1. 第一层:VxeGridProps<D>
源码里(简化):
export interface VxeGridProps<D = any> extends VxeTableProps<D> {
columns?: VxeGridPropTypes.Columns<D>
proxyConfig?: VxeGridPropTypes.ProxyConfig<D>
// ...
}
当你用 VxeGridProps<RowVO> 时:
extends VxeTableProps<D>→ 变成extends VxeTableProps<RowVO>columns?: Columns<D>→ 变成columns?: Columns<RowVO>proxyConfig?: ProxyConfig<D>→ 变成proxyConfig?: ProxyConfig<RowVO>
记忆:哪里写了
<D>,就会被替换成<RowVO>。
2. 第二层:Columns<D> = Column<D>[]
export namespace VxeGridPropTypes {
export type Column<D = any> = VxeTableDefines.ColumnOptions<D>
export type Columns<D = any> = Column<D>[]
}
当你用的是 Columns<RowVO> 时:
Columns<D>→Columns<RowVO>= Column<D>[]这一行里的D同样被替换成RowVO,变成:Columns<RowVO> = Column<RowVO>[]
接着:
Column<D> = VxeTableDefines.ColumnOptions<D>- 也会变成:
Column<RowVO> = VxeTableDefines.ColumnOptions<RowVO>
所以:
columns的每一项类型就是ColumnOptions<RowVO>。
七、第三层:ColumnOptions<D> 里 D 真正用在哪里?
export interface ColumnOptions<D = any> extends VxeColumnProps<D> {
children?: ColumnOptions<D>[]
slots?: VxeColumnPropTypes.Slots<D>
}
继续替换:
ColumnOptions<D>→ColumnOptions<RowVO>extends VxeColumnProps<D>→ 变成extends VxeColumnProps<RowVO>children?: ColumnOptions<D>[]→ 变成children?: ColumnOptions<RowVO>[]slots?: Slots<D>→ 变成slots?: Slots<RowVO>
关键点:ColumnOptions<RowVO> 本身定义了「列配置」的结构。它继承的 VxeColumnProps<RowVO> + Slots<RowVO> 等地方,会在「需要行数据的回调」里用到 RowVO,比如:
formatter(params: { row: RowVO; ... })
className(params: { row: RowVO; ... })
八、我在学习时候的疑惑?
我当时并不理解 TypeScript 做的是统一替换:
// 把 D 换成 RowVO:
type Columns<RowVO> = Column<RowVO>[]
// 再把 Column 展开:
type Column<RowVO> = VxeTableDefines.ColumnOptions<RowVO>
// 合起来就是:
type Columns<RowVO> = VxeTableDefines.ColumnOptions<RowVO>[]
就拿文章示例的代码来看,TypeScript 会把函数体中所有的 <T> 都替换成你指定的类型。
不理解的代码:
export type Column<D = any> = VxeTableDefines.ColumnOptions<D>
export type Columns<D = any> = Column<D>[]
我当时特别不能理解 Column<D>[] 的 <D> 是怎么变成 <RowVO> 的。直到我明白了 TypeScript 会做统一替换,根本不是按数据传参的逻辑去做的。
九、把整个链路串起来(从外到内)
你写了:
reactive<VxeGridProps<RowVO>>({...})
于是:
VxeGridProps<D> → VxeGridProps<RowVO>
extends VxeTableProps<D> → extends VxeTableProps<RowVO>
columns?: Columns<D> → columns?: Columns<RowVO>
然后:
Columns<D> = Column<D>[] → Columns<RowVO> = Column<RowVO>[]
Column<D> = ColumnOptions<D> → Column<RowVO> = ColumnOptions<RowVO>
再往下:
ColumnOptions<D> extends VxeColumnProps<D> → ColumnOptions<RowVO> extends VxeColumnProps<RowVO>
最终效果:
data的类型是:RowVO[]- 所有回调里涉及「行数据」的地方,类型参数都是
RowVO
十、总结这次案例
RowVO:描述「一行数据长什么样」VxeGridProps<RowVO>:告诉表格「我的每一行数据都是RowVO」- 泛型参数
<RowVO>会一层层往下传,凡是类型里写了<D>的地方,就会变成<RowVO>
你现在已经不是「不懂泛型的小白」了,你已经能:
- 看懂「类型参数是怎么一层一层传下去的」
- 顺着
VxeGridProps<RowVO> → Columns<RowVO> → ColumnOptions<RowVO>这一整条链路往下追
这就已经是非常扎实的泛型理解了。
总结
- 泛型的核心是「给类型加参数」,使用时再指定具体类型,如
Array<string>、VxeGridProps<RowVO> - 嵌套泛型的本质是「类型参数本身也是泛型」,参数会逐层传递替换(
D→RowVO)
学习本就是一场持久战,不需要急着一口吃成胖子。哪怕今天你只记住了一点点,这都是实打实的进步。
后续我还会继续用这种大白话、讲实战的方式,带大家扫盲更多前端基础。
关注我,不迷路,咱们把那些曾经模糊的知识点,一个个彻底搞清楚。
如果你觉得这篇内容对你有帮助,不妨点赞 + 收藏,下次写代码卡壳时,拿出来翻一翻,比搜引擎更靠谱。
我是 Eugene,你的电子学友,我们下一篇干货见~