TypeScript 泛型完全理解指南
我死磕了很久才搞懂的 TypeScript 泛型,今天用"模具"讲明白了

写在前面
如果你跟我一样,泛型这玩意儿写了无数遍,每次照着语法抄一遍能跑起来,但合上电脑就完全说不清"它到底在干嘛"——这篇文章就是写给你的。
不讲术语,先讲一个比喻。
一、先理解:泛型到底解决什么问题
假设你要写一个函数,取数组的第一个元素:
function getFirst(arr: number[]): number {
return arr[0]
}
这个函数只能处理 number 数组。如果你想处理字符串数组呢?逻辑一模一样,但你得再写一个:
function getFirstString(arr: string[]): string {
return arr[0]
}
代码逻辑完全相同,只是类型不一样,却要复制粘贴一份。这就是泛型要解决的问题:逻辑相同,类型不同,怎么避免重复造轮子。
二、一个比喻:泛型就是"模具"
我后来理解泛型,是靠这一个画面:
泛型就是一个模具,不是具体的杯子,而是"杯子的形状"。
你往模具里倒什么材料,出来的就是什么材质的杯子——但形状(也就是逻辑)永远不变。
function box<T>(value: T): T[] {
return [value]
}
box(123) // 倒进去 number,出来 number[] 形状的杯子
box('hi') // 倒进去 string,出来 string[] 形状的杯子
box 这个函数就是模具。T 是"先空着,等会儿再填"的占位符。你不需要为每种材料重新做一个模具——这是泛型存在的唯一理由。
记住这一句话就够了:
看到尖括号
<T>,脑子里直接翻译成"这里的类型先空着,等用的时候再告诉它具体是什么"。
三、判断要不要用泛型,只有一条标准
这是我后来总结出来、也是验证下来最好用的一条判断标准:
如果函数内部的操作只依赖类型里的某个固定特征(比如
.length、% 2),就不需要泛型;
如果函数内部根本不关心传进来的是什么类型,只是原样传递、存储或重新排列,那就是泛型该登场的地方。
拿几个例子对照一下,你会发现这条标准非常好用:
| 场景 | 要不要泛型 | 为什么 |
|---|---|---|
| 判断一个数是不是偶数 | ❌ 不要 | % 2 这个操作只能用在 number 上,类型天生固定 |
| 交换两个值的位置 | ✅ 要 | 交换这个动作跟值具体是什么类型完全无关,number、string、对象都一样能交换 |
| 取字符串长度 | ❌ 不要 | .length 只在字符串上有意义,类型写死就行 |
| 把任意值包装成单元素数组 | ✅ 要 | 包装这个动作跟值的类型毫无关系 |
一句话总结:函数摸了类型里的某个具体特征,就别用泛型;函数压根没摸类型,只是搬运,那就该用泛型。
四、语法到底怎么读:先声明,再使用
很多人(包括我)第一次看到这种写法会卡住:
function first<T>(arr: T[]): T {
return arr[0]
}
诀窍是把这一行拆成两步看:
<T> ← ① 先"声明":我要用一个叫 T 的类型占位符
T[]、T ← ② 然后才能"使用"这个占位符,去描述参数类型和返回值类型
T[] 这个写法你其实很熟悉,它跟 number[]、string[] 是同一套语法规则,只是把具体类型换成了占位符:
number[] // 元素是 number 的数组
string[] // 元素是 string 的数组
T[] // 元素是 T 的数组,T 具体是什么还不知道,等调用时才确定
调用的时候,T 自动被填上具体类型:
first([1, 2, 3])
// T 被推断成 number
// arr: T[] 这里就变成了 arr: number[]
// 返回类型 T 也变成了 number
可以理解成:变量要先 let T 才能用 T,只不过泛型里"声明"的位置固定写在函数名后面的尖括号里。
五、两种"填模具"的方式
第一种:自动推断——这是大多数场景,TS 会根据你传入的实际值反推类型:
function wrap<T>(value: T): T[] {
return [value]
}
wrap(123) // TS 自动推断 T = number,不用你操心
第二种:手动指定——当函数压根没传值,TS 没法从值反推类型的时候,你就得自己在尖括号里写明白:
const arr = wrap<string>('hello')
// ^^^^^^^^ 手动告诉 TS,T 就是 string
这种场景在日常开发里其实很常见。比如 Vue 的 defineProps:
defineProps<{ title: string }>()
// 圆括号是空的,没有值可供推断,只能在尖括号里手动指定类型
再比如 React 的 useState,配合一个空的初始值时:
// 不写泛型,TS 会把 user 类型锁死成 null,之后赋值别的类型会报错
const [user, setUser] = useState(null)
// 手动指定泛型,告诉 TS:虽然现在是 null,但之后可能变成 User
const [user, setUser] = useState<User | null>(null)
六、泛型用在哪几个地方
① 函数(前面已经看过了)
function first<T>(arr: T[]): T {
return arr[0]
}
② interface / type——做接口请求时特别好用,可以写一个通用的响应结构:
interface ApiResponse<T> {
data: T
status: number
}
type User = { id: number; name: string }
type Product = { id: number; price: number }
function fetchData<T>(url: string): Promise<ApiResponse<T>> {
return fetch(url).then(res => res.json())
}
const userRes = await fetchData<User>('/api/user') // data 是 User 类型
const productRes = await fetchData<Product>('/api/product') // data 是 Product 类型
③ React Hook(日常最常碰到的场景)
useState<number>(0)
useState<User | null>(null)
④ class
class Box<T> {
content: T
constructor(value: T) {
this.content = value
}
}
const numberBox = new Box<number>(123)
const stringBox = new Box<string>('hello')
七、进阶一点:泛型约束 (extends)
默认情况下 T 可以是任何类型,但有时候你需要限制它至少满足某个条件,比如至少要有 .length 属性:
// 没有约束,报错:因为 T 可能是 number,而 number 没有 .length
function printLength<T>(item: T) {
console.log(item.length) // ❌
}
// 加上约束:T 必须是"有 length 属性"的类型
function printLength<T extends { length: number }>(item: T) {
console.log(item.length) // ✅
}
printLength('hello') // ✅ 字符串有 length
printLength([1, 2, 3]) // ✅ 数组有 length
printLength(123) // ❌ number 没有 length,报错
extends 在这里不是面向对象那个"继承"的意思,是在说"T 必须长这个样子,至少要满足这个结构"。
八、顺带提一句:函数参数解构 + 类型标注,也是一个常见的卡点
学泛型的过程中,我还被另一个长得很像但完全无关的写法绊住过:
function RootLayout({ children }: { children: React.ReactNode }) {
return <div>{children}</div>
}
这一行其实是两件独立的事挤在一起,恰好都用了花括号,容易看串:
{ children } ← JS 的解构赋值(拿到参数对象里的 children 属性)
{ children: React.ReactNode } ← TS 的类型标注(说明这个参数长什么样)
冒号 : 是分界线,左边是解构,右边是类型,两者其实没有关系,只是因为长得像才让人犯迷糊。如果字段多了,建议拆开写:
type RootLayoutProps = {
children: React.ReactNode
}
function RootLayout({ children }: RootLayoutProps) {
return <div>{children}</div>
}
可读性立刻好很多。
九、最后,一张速查表收尾
| 看到这个写法 | 直接这样理解 |
|---|---|
<T>、<User>、<number> |
这里的类型先空着,等用的时候再告诉它具体是什么 |
{ x }: { x: Type } |
冒号左边是解构(JS),右边是类型(TS),两件独立的事 |
T[] |
“元素类型为 T 的数组”,跟 number[] 是同一套写法规则 |
<T extends {...}> |
T 可以是任何类型,但必须至少满足这个结构 |
如果你也是看了无数遍语法但脑子里一直没有画面的人,希望"模具"这个比喻能帮你也接上这条线。理解到这一步之后再回头看语法,会发现其实没那么复杂——卡住我们的从来不是语法本身,而是没人帮我们先画出那张画面。
更多推荐




所有评论(0)