从「完全不懂泛型」一路走到「看懂下面这段代码到底在干嘛」:
//此代为为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官网
一、什么是泛型?一句话版本
泛型 = 给 “类型” 加参数。
- 函数可以有参数:
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 | KeyKey | 表示对象的「键」类型 | 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) |
六、类型参数是怎么一层一层 “传下去” 的?
到这一步为了更好的理解泛型,我将带着同学们追溯源码。一起来追踪一下源码看看吧。
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);
以上便是对泛型的分享,欢迎大家指正讨论,与大家共勉。