刚开始学 TS 的时候看到泛型就头疼——<T> 是什么意思?为什么要写这么长一串?直到有一天不用泛型写不下去了,才发现这东西是真的好用。
没有泛型的痛点
// 不用泛型:要么写死类型,要么用 any
function getFirst(arr: number[]): number {
return arr[0]
}
// 只能传 number[],传 string[] 就报错
function getFirstAny(arr: any[]): any {
return arr[0]
}
// 返回值是 any,丢失了类型信息
const result = getFirstAny(['a', 'b'])
// result 是 any,TS 不会帮你检查
泛型就是"类型参数"
function getFirst<T>(arr: T[]): T {
return arr[0]
}
const a = getFirst([1, 2, 3]) // T = number,a 是 number
const b = getFirst(['a', 'b', 'c']) // T = string,b 是 string
const c = getFirst<boolean>([]) // 手动指定 T = boolean
<T> 的意思是:这个函数的参数和返回值类型由调用时决定。T 就是一个占位符。
泛型约束
// T 必须有 length 属性
function getLength<T extends { length: number }>(item: T): number {
return item.length
}
getLength('hello') // OK,string 有 length
getLength([1, 2, 3]) // OK,array 有 length
getLength(123) // Error,number 没有 length
extends 不是继承,是"约束"——T 必须满足某个条件。
泛型接口
interface ApiResponse<T> {
code: number
message: string
data: T
}
// 使用
type UserResponse = ApiResponse<User>
// { code: number; message: string; data: User }
type ListResponse = ApiResponse<User[]>
// { code: number; message: string; data: User[] }
function request<T>(url: string): Promise<ApiResponse<T>> {
return fetch(url).then(res => res.json())
}
// 调用时指定返回类型
const user = await request<User>('/api/users/1')
// user.data 的类型是 User,有完整的类型提示
常用内置泛型
// Partial — 所有属性变可选
type PartialUser = Partial<User>
// { name?: string; age?: number; email?: string }
// Required — 所有属性变必填
type RequiredUser = Required<Partial<User>>
// Pick — 挑选部分属性
type UserBasic = Pick<User, 'name' | 'age'>
// { name: string; age: number }
// Omit — 排除部分属性
type UserWithoutEmail = Omit<User, 'email'>
// { name: string; age: number }
// Record — 键值对类型
type StringMap = Record<string, string>
// { [key: string]: string }
// Exclude / Extract — 联合类型过滤
type A = 'a' | 'b' | 'c'
type B = 'a' | 'c'
type OnlyA = Exclude<A, B> // 'b'
type Common = Extract<A, B> // 'a' | 'c'
泛型组件
React 组件也可以用泛型:
interface ListProps<T> {
items: T[]
renderItem: (item: T) => React.ReactNode
}
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map(renderItem)}</ul>
}
// 使用
<List
items={[{ id: 1, name: 'Alice' }, { id: 2, name: 'Bob' }]}
renderItem={(user) => <li key={user.id}>{user.name}</li>}
/>
// TS 自动推断 user 的类型
多类型参数
function merge<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 }
}
const result = merge({ name: 'Alice' }, { age: 25 })
// result: { name: string } & { age: number }
// 即 { name: string; age: number }
// 键值映射
function mapObject<T, K extends keyof T, V>(
obj: T,
key: K,
transform: (value: T[K]) => V
): Record<string, V> {
// ...
}
泛型其实很简单
泛型的本质就是"让类型也变成参数"。跟函数参数一样——你不需要写死值,调用时传就行。泛型让你不需要写死类型,使用时指定就行。
看到 <T> 不要慌,它就是一个"还没确定是什么类型"的占位符。等调用的时候,TS 会根据你传的参数自动推断。
总结
泛型 = 类型参数。<T> 是占位符,extends 是约束。内置的 Partial、Pick、Omit、Record 都是泛型实现的。日常开发中最常用在 API 响应类型、通用组件、工具函数里。不用一开始就学高级泛型,把基础的 <T> 和 extends 搞懂就够用了。