TypeScript 泛型入门,从看不懂到离不开

0 阅读2分钟

刚开始学 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 搞懂就够用了。