Skip to content

TypeScript 泛型

大白话解释: 泛型就是"万能遥控器"。普通遥控器只能控制一个品牌的电视,万能遥控器不管什么品牌都能用。泛型让函数/类支持多种类型,不用为每种类型写一遍代码。

为什么要用泛型?

  • 代码复用:写一次,支持多种类型
  • 类型安全:编译时检查类型,不会出现运行时错误
  • 灵活扩展:可以添加约束,限制泛型的范围

先理解问题:为什么需要泛型?

没有泛型的痛苦

ts
// 假设你要写一个"返回第一个元素"的函数

// 方案1:用 any(不推荐)
function firstAny(arr: any[]): any {
  return arr[0]
}
// 问题:丢失了类型信息
const result1 = firstAny([1, 2, 3])  // any 类型,不知道是 number

// 方案2:为每种类型写一个函数(太累了)
function firstNumber(arr: number[]): number {
  return arr[0]
}
function firstString(arr: string[]): string {
  return arr[0]
}
function firstBoolean(arr: boolean[]): boolean {
  return arr[0]
}
// 问题:代码重复,100种类型就要写100个函数

// 方案3:用泛型(推荐)
function first<T>(arr: T[]): T {
  return arr[0]
}
// 完美:一个函数搞定所有类型,而且保留了类型信息
const num = first([1, 2, 3])      // number 类型
const str = first(['a', 'b', 'c']) // string 类型

泛型基础

泛型函数

ts
// ---- 泛型函数:让函数支持多种类型 ----
// 大白话:T 就像一个"占位符",调用时才确定具体是什么类型

// 基本泛型函数
function identity<T>(arg: T): T {
  return arg
}
// 解读:
// <T>:声明一个类型参数 T
// arg: T:参数的类型是 T
// : T:返回值的类型也是 T
// 调用时 T 会被替换成实际类型

// 使用方式1:手动指定类型(不常用)
const num = identity<number>(42)    // T = number
const str = identity<string>('hello') // T = string

// 使用方式2:让 TS 自动推断(推荐)
const num2 = identity(42)      // TS 自动推断 T = number
const str2 = identity('hello') // TS 自动推断 T = string

// ---- 多个泛型参数 ----
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second]
}
// T 和 U 是两个不同的类型参数,可以不同
const p1 = pair('hello', 42)   // [string, number]
const p2 = pair(42, true)      // [number, boolean]
const p3 = pair('a', 'b')      // [string, string]

// ---- 泛型约束:限制 T 必须满足某些条件 ----
interface HasLength {
  length: number
}

// T 必须有 length 属性(用 extends 关键字)
function logLength<T extends HasLength>(arg: T): T {
  console.log(arg.length)  // 安全,因为 T 一定有 length
  return arg
}

logLength('hello')      // ✅ string 有 length(5)
logLength([1, 2, 3])    // ✅ array 有 length(3)
logLength({ length: 5 }) // ✅ 有 length 属性
// logLength(42)         // ❌ number 没有 length
// logLength(true)       // ❌ boolean 没有 length

泛型接口

ts
// ---- 泛型接口:让接口支持不同类型 ----
// 大白话:就像"快递箱",箱子的形状固定,但里面装的东西可以是任意类型

// 泛型接口
interface ApiResponse<T> {
  code: number
  message: string
  data: T  // T 是占位符,使用时才确定
}

// 使用:指定 T 为 User
interface User {
  id: number
  name: string
}

const response: ApiResponse<User> = {
  code: 200,
  message: 'success',
  data: { id: 1, name: '张三' },  // data 的类型是 User
}

// 使用:指定 T 为 User[]
const listResponse: ApiResponse<User[]> = {
  code: 200,
  message: 'success',
  data: [{ id: 1, name: '张三' }],  // data 的类型是 User[]
}

// ---- 泛型接口的实际应用 ----

// 列表响应
interface ListResponse<T> {
  items: T[]
  total: number
  page: number
  pageSize: number
}

// 继承泛型接口
interface UserListResponse extends ListResponse<User> {
  // 继承后,items 自动变成 User[]
}

const userList: UserListResponse = {
  items: [{ id: 1, name: '张三' }],
  total: 100,
  page: 1,
  pageSize: 10,
}

// 泛型函数接口
interface Factory<T> {
  create(): T
  validate(item: T): boolean
}

class UserFactory implements Factory<User> {
  create(): User {
    return { id: Date.now(), name: '' }
  }

  validate(user: User): boolean {
    return user.name.length > 0
  }
}

泛型类

ts
// ---- 泛型类:让类支持不同类型 ----
// 大白话:就像"栈"这个数据结构,不管存数字还是字符串,操作方式都一样

class Stack<T> {
  private items: T[] = []

  push(item: T): void {
    this.items.push(item)
  }

  pop(): T | undefined {
    return this.items.pop()
  }

  peek(): T | undefined {
    return this.items[this.items.length - 1]
  }

  isEmpty(): boolean {
    return this.items.length === 0
  }

  size(): number {
    return this.items.length
  }
}

// 使用:存数字
const numberStack = new Stack<number>()
numberStack.push(1)
numberStack.push(2)
numberStack.push(3)
console.log(numberStack.pop())  // 3
// numberStack.push('hello')    // ❌ 错误,不能存 string

// 使用:存字符串
const stringStack = new Stack<string>()
stringStack.push('hello')
stringStack.push('world')
console.log(stringStack.peek())  // 'world'

// ---- 实际应用:通用队列 ----
class Queue<T> {
  private items: T[] = []

  enqueue(item: T): void {
    this.items.push(item)
  }

  dequeue(): T | undefined {
    return this.items.shift()
  }

  front(): T | undefined {
    return this.items[0]
  }

  isEmpty(): boolean {
    return this.items.length === 0
  }
}

const queue = new Queue<string>()
queue.enqueue('任务1')
queue.enqueue('任务2')
queue.enqueue('任务3')
console.log(queue.dequeue())  // '任务1'

泛型约束

extends 约束

ts
// ---- extends 约束:限制泛型必须满足某些条件 ----
// 大白话:就像"入场券",必须满足条件才能进来

// 约束 T 必须有 length 属性
interface HasLength {
  length: number
}

function logLength<T extends HasLength>(arg: T): T {
  console.log(arg.length)
  return arg
}

// 约束 T 必须是某个类的实例
class Animal {
  name: string
  constructor(name: string) {
    this.name = name
  }
}

class Dog extends Animal {
  bark() {
    console.log('汪汪叫')
  }
}

// T 必须是 Animal 或其子类
function createAnimal<T extends Animal>(ctor: new (name: string) => T, name: string): T {
  return new ctor(name)
}

const dog = createAnimal(Dog, '旺财')  // Dog 类型
dog.bark()  // 汪汪叫

// 约束 T 必须是对象类型
function merge<T extends object, U extends object>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 }
}

const merged = merge({ name: '张三' }, { age: 25 })
// { name: string; age: number }

keyof 约束

ts
// ---- keyof 约束:限制 key 必须是对象的属性 ----
// 大白话:就像"只能用钥匙开门",不能用别的东西

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]
}

const user = { name: '张三', age: 25 }
const name = getProperty(user, 'name')  // string 类型
const age = getProperty(user, 'age')    // number 类型
// const email = getProperty(user, 'email')  // ❌ 'email' 不是 user 的属性

// 解读:
// T:对象的类型
// K extends keyof T:K 必须是 T 的某个属性名
// T[K]:返回值的类型就是 T 中 K 对应的类型

// ---- 实际应用:安全地设置对象属性 ----
function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]): void {
  obj[key] = value
}

setProperty(user, 'name', '李四')  // ✅
// setProperty(user, 'name', 123)    // ❌ 类型不匹配
// setProperty(user, 'email', 'xxx') // ❌ 'email' 不存在

泛型默认值

ts
// ---- 泛型默认值:给泛型参数一个默认类型 ----
// 大白话:就像"默认选项",不指定就用默认的

interface ApiResponse<T = unknown> {
  code: number
  message: string
  data: T
}

// 使用默认类型(T = unknown)
const response1: ApiResponse = {
  code: 200,
  message: 'success',
  data: 'some data',  // data 类型为 unknown
}

// 指定类型(T = User)
const response2: ApiResponse<User> = {
  code: 200,
  message: 'success',
  data: { id: 1, name: '张三' },  // data 类型为 User
}

// ---- 多个泛型参数 ----
interface EventHandler<T = Event, U = void> {
  (event: T): U
}

// 使用默认值(T = Event, U = void)
const clickHandler: EventHandler = (event) => {
  console.log(event.target)
}

// 只指定第一个参数(T = KeyboardEvent, U = void)
const keyHandler: EventHandler<KeyboardEvent> = (event) => {
  console.log(event.key)
}

// 指定两个参数(T = SubmitEvent, U = boolean)
const submitHandler: EventHandler<SubmitEvent, boolean> = (event) => {
  event.preventDefault()
  return true
}

泛型工具类型

更多工具类型详见 TypeScript 类型系统

ts
// ---- 内置工具类型:TS 帮你写好的"类型函数" ----
// 大白话:就像"瑞士军刀",常用的类型变换工具都有了

interface User {
  id: number
  name: string
  email?: string
}

// Partial<T>:所有属性变为可选
type PartialUser = Partial<User>
// { id?: number; name?: string; email?: string }

// Required<T>:所有属性变为必需
type RequiredUser = Required<User>
// { id: number; name: string; email: string }

// Readonly<T>:所有属性变为只读
type ReadonlyUser = Readonly<User>
// { readonly id: number; readonly name: string; readonly email?: string }

// Pick<T, K>:选取部分属性
type UserBasic = Pick<User, 'id' | 'name'>
// { id: number; name: string }

// Omit<T, K>:排除部分属性
type UserWithoutEmail = Omit<User, 'email'>
// { id: number; name: string }

// Record<K, V>:构造键值对类型
type UserMap = Record<string, User>
// { [key: string]: User }

高级泛型模式

条件类型

ts
// ---- 条件类型:根据条件决定类型 ----
// 大白话:就像"三元运算符",但用在类型上
// 语法:T extends U ? X : Y(如果 T 是 U 的子类型,就是 X,否则是 Y)

type IsString<T> = T extends string ? true : false

type A = IsString<string>  // true
type B = IsString<number>  // false

// 实际应用:过滤类型
type Exclude<T, U> = T extends U ? never : T
type Extract<T, U> = T extends U ? T : never

type Status = 'active' | 'inactive' | 'pending'
type ActiveStatus = Exclude<Status, 'inactive' | 'pending'>  // 'active'
type InactiveStatus = Extract<Status, 'inactive' | 'pending'>  // 'inactive' | 'pending'

// 分布式条件类型:联合类型的每个成员都应用条件
type ToArray<T> = T extends any ? T[] : never

type C = ToArray<string | number>  // string[] | number[]
// 等价于 ToArray<string> | ToArray<number> = string[] | number[]

infer 关键字

ts
// ---- infer:在条件类型中"捕获"类型 ----
// 大白话:就像"抓娃娃机",从类型中抓出你想要的部分

// 提取函数返回类型
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never
// 解读:如果 T 是函数类型,就"抓出"返回值类型 R,否则是 never

type D = ReturnType<() => string>  // string
type E = ReturnType<(x: number) => boolean>  // boolean

// 提取函数参数类型
type Parameters<T> = T extends (...args: infer P) => any ? P : never
// 解读:如果 T 是函数类型,就"抓出"参数类型 P

type F = Parameters<(a: string, b: number) => void>  // [a: string, b: number]

// 提取数组元素类型
type ElementType<T> = T extends (infer E)[] ? E : never
// 解读:如果 T 是数组类型,就"抓出"元素类型 E

type G = ElementType<string[]>  // string
type H = ElementType<number[]>  // number

// 提取 Promise 的值类型
type Awaited<T> = T extends Promise<infer U> ? U : T

type I = Awaited<Promise<string>>  // string
type J = Awaited<Promise<number>>  // number

映射类型

ts
// ---- 映射类型:遍历对象的 key,生成新的类型 ----
// 大白话:就像"批量加工",把对象的每个属性都做同样的变换

// 生成 getter 类型
type Getters<T> = {
  [P in keyof T as `get${Capitalize<string & P>}`]: () => T[P]
}

interface User {
  id: number
  name: string
  email: string
}

type UserGetters = Getters<User>
// {
//   getId: () => number
//   getName: () => string
//   getEmail: () => string
// }

// 实际应用:表单状态
type FormState<T> = {
  [P in keyof T]: {
    value: T[P]
    error: string | null
    touched: boolean
  }
}

interface LoginForm {
  username: string
  password: string
}

type LoginFormState = FormState<LoginForm>
// {
//   username: { value: string; error: string | null; touched: boolean }
//   password: { value: string; error: string | null; touched: boolean }
// }

实际应用示例

API 请求封装

ts
// ---- 泛型 API 请求函数 ----
// 大白话:就像"万能快递员",不管寄什么类型的包裹都能处理

interface ApiResponse<T> {
  code: number
  message: string
  data: T
}

async function request<T>(
  url: string,
  options?: RequestInit
): Promise<ApiResponse<T>> {
  const response = await fetch(url, options)
  return response.json()
}

// 使用:指定 T 为 User,返回值自动有类型
interface User {
  id: number
  name: string
  email: string
}

const response = await request<User>('/api/user/1')
console.log(response.data.name)  // 类型安全,编辑器有提示

// 封装更高级的 API 客户端
class ApiClient {
  constructor(private baseUrl: string) {}

  async get<T>(endpoint: string): Promise<T> {
    const response = await fetch(`${this.baseUrl}${endpoint}`)
    return response.json()
  }

  async post<T>(endpoint: string, data: any): Promise<T> {
    const response = await fetch(`${this.baseUrl}${endpoint}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
    })
    return response.json()
  }
}

const api = new ApiClient('https://api.example.com')
const users = await api.get<User[]>('/users')  // User[] 类型
const newUser = await api.post<User>('/users', { name: '张三' })  // User 类型

状态管理

ts
// ---- 泛型状态管理 ----
// 大白话:就像"万能仓库",不管存什么类型的货物都能管理

class Store<T extends Record<string, any>> {
  // T extends Record<string, any>:T 必须是对象类型(不能是 number、string 等)
  // Record<string, any> 表示"键是字符串,值是任意类型"的对象
  private state: T
  private listeners: Set<() => void> = new Set()

  constructor(initialState: T) {
    this.state = initialState
  }

  getState(): T {
    return this.state
  }

  // 为什么用 Partial<T>?
  // 因为更新状态时,通常只需要修改部分属性,不需要传整个对象
  // 例如:store.setState({ loading: true }) 只更新 loading,不传 user
  // Partial<T> 让 T 的所有属性变成可选,这样可以只传需要更新的字段
  setState(partial: Partial<T>): void {
    // 用展开运算符合并:保留旧状态,覆盖新状态
    this.state = { ...this.state, ...partial }
    // 通知所有订阅者:状态变了,该更新了
    this.listeners.forEach(listener => listener())
  }

  subscribe(listener: () => void): () => void {
    this.listeners.add(listener)
    // 返回取消订阅函数:调用后不再监听状态变化
    return () => this.listeners.delete(listener)
  }
}

// 使用
interface AppState {
  user: User | null
  theme: 'light' | 'dark'
  loading: boolean
}

const store = new Store<AppState>({
  user: null,
  theme: 'light',
  loading: false,
})

// Partial<AppState> = { user?: User | null; theme?: 'light' | 'dark'; loading?: boolean }
// 所以可以只传部分属性
store.setState({ loading: true })  // ✅ 只更新 loading
store.setState({ user: { id: 1, name: '张三', email: 'xxx' } })  // ✅ 只更新 user
// store.setState({ xxx: 123 })  // ❌ xxx 不是 AppState 的属性

表单验证

ts
// ---- 泛型表单验证器 ----
// 大白话:就像"万能检查员",不管检查什么类型的表单都能用

// 验证规则接口:每个规则有一个验证函数和错误提示
interface ValidationRule<T> {
  validate: (value: T) => boolean  // 验证函数:返回 true 表示通过
  message: string                  // 验证失败时的错误提示
}

// T extends Record<string, any>:T 必须是对象类型(表单数据)
// Record<string, any> 表示键值对对象,确保 T 有字段可以验证
class FormValidator<T extends Record<string, any>> {
  // Map 的 key 是表单字段名(如 'username'),value 是该字段的验证规则数组
  // keyof T 保证 key 一定是 T 的属性名,不会拼错
  private rules: Map<keyof T, ValidationRule<T[keyof T]>[]> = new Map()

  // K extends keyof T:K 必须是 T 的某个属性名(如 'username')
  // T[K]:该属性的类型(如 string)
  // 返回 this:支持链式调用(validator.addRule(...).addRule(...))
  addRule<K extends keyof T>(field: K, rule: ValidationRule<T[K]>): this {
    const existing = this.rules.get(field) || []
    this.rules.set(field, [...existing, rule] as any)
    return this
  }

  // 返回值:valid 表示整体是否通过,errors 是每个字段的错误信息
  // Partial<Record<keyof T, string>>:错误信息对象,每个字段可选
  validate(data: T): { valid: boolean; errors: Partial<Record<keyof T, string>> } {
    const errors: Partial<Record<keyof T, string>> = {}
    let valid = true

    // 遍历所有字段的验证规则
    for (const [field, rules] of this.rules) {
      for (const rule of rules) {
        // data[field] 获取该字段的值,传给验证函数
        if (!rule.validate(data[field])) {
          errors[field] = rule.message  // 记录错误信息
          valid = false
          break  // 一个字段只要有一个规则失败就停止
        }
      }
    }

    return { valid, errors }
  }
}

// 使用
interface FormData {
  username: string
  email: string
  age: number
}

// 泛型参数 FormData 保证 addRule 只能用 'username' | 'email' | 'age' 作为字段名
const validator = new FormValidator<FormData>()
  .addRule('username', {
    validate: (value) => value.length >= 3,  // value 自动推断为 string
    message: '用户名至少 3 个字符',
  })
  .addRule('email', {
    validate: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),  // value 是 string
    message: '邮箱格式不正确',
  })
  .addRule('age', {
    validate: (value) => value >= 18,  // value 自动推断为 number
    message: '年龄必须大于等于 18',
  })

const result = validator.validate({
  username: 'ab',
  email: 'invalid',
  age: 16,
})
// { valid: false, errors: { username: '...', email: '...', age: '...' } }

通用事件系统

ts
// ---- 泛型事件系统 ----
// 大白话:就像"万能事件中心",不管什么类型的事件都能处理

class EventEmitter<TEvents extends Record<string, any>> {
  private listeners: Map<keyof TEvents, Set<Function>> = new Map()

  on<K extends keyof TEvents>(event: K, handler: (data: TEvents[K]) => void): void {
    if (!this.listeners.has(event)) {
      this.listeners.set(event, new Set())
    }
    this.listeners.get(event)!.add(handler)
  }

  off<K extends keyof TEvents>(event: K, handler: (data: TEvents[K]) => void): void {
    this.listeners.get(event)?.delete(handler)
  }

  emit<K extends keyof TEvents>(event: K, data: TEvents[K]): void {
    this.listeners.get(event)?.forEach(handler => handler(data))
  }
}

// 使用
interface AppEvents {
  login: { userId: string; timestamp: Date }
  logout: { userId: string }
  error: { message: string; code: number }
}

const emitter = new EventEmitter<AppEvents>()

emitter.on('login', (data) => {
  console.log(`用户 ${data.userId} 登录`)  // data 有类型提示
})

emitter.emit('login', { userId: '123', timestamp: new Date() })
// emitter.emit('unknown', {})  // ❌ 事件名不存在

常见问题

泛型类型推断失败

ts
// 问题:类型推断失败
function merge<T, U>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 }
}

// 解决:明确指定类型
const result = merge<User, { role: string }>(
  { id: 1, name: '张三', email: '[email protected]' },
  { role: 'admin' }
)

泛型约束过宽

ts
// 问题:约束过宽,T 可能没有 length 属性
function getLength<T>(arg: T): number {
  return arg.length  // ❌ 类型 'T' 上不存在属性 'length'
}

// 解决:添加约束
function getLength<T extends { length: number }>(arg: T): number {
  return arg.length  // ✅
}

泛型默认值与约束冲突

ts
// 问题:默认值不满足约束(TS 报错:Type '{ name: string }' does not satisfy the constraint)
interface Container<T extends { id: number } = { name: string }> {
  data: T
}

// 解决:确保默认值满足约束(默认类型必须 extends 约束类型)
interface Container<T extends { id: number } = { id: number; name: string }> {
  data: T
}

参考


Vue/Vite 项目实际示例

泛型 Composable:useFetch

ts
// ---- 泛型 Composable:通用数据请求 ----
// 为什么用泛型?因为不同的 API 返回的数据类型不同
// 泛型让 useFetch 能适配任何数据类型,同时保留类型信息

import { ref, readonly } from 'vue'

// T 是返回数据的类型,默认 unknown
function useFetch<T = unknown>(url: string) {
  // ref<T | null>:数据可能是 T 类型,也可能是 null(初始状态)
  const data = ref<T | null>(null)
  const loading = ref(false)
  const error = ref<string | null>(null)

  async function execute() {
    loading.value = true
    error.value = null
    try {
      const response = await fetch(url)
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`)
      }
      // as T:告诉 TS 响应体的类型是 T
      data.value = await response.json() as T
    } catch (e) {
      // e 是 unknown 类型,需要类型断言
      error.value = e instanceof Error ? e.message : '请求失败'
    } finally {
      loading.value = false
    }
  }

  return {
    data: readonly(data),      // 只读,外部不能直接修改
    loading: readonly(loading),
    error: readonly(error),
    execute,                   // 调用后才发请求
  }
}

// 使用:指定 T 为 User[],data 自动变成 User[] | null
interface User {
  id: number
  name: string
  email: string
}

const { data: users, loading, error, execute } = useFetch<User[]>('/api/users')

// users.value 的类型是 User[] | null,编辑器有完整提示
// users.value?.[0].name  // ✅ 类型安全

泛型组件类型工具

ts
// ---- 泛型组件类型工具 ----
// 场景:需要引用子组件实例,但不知道具体类型

import { ref } from 'vue'

// 获取组件实例类型
type ComponentInstance<T> = T extends new (...args: any[]) => infer I ? I : never

// 通用的组件引用 Hook
function useComponentRef<T extends abstract new (...args: any) => any>() {
  return ref<InstanceType<T>>()
}

// 实际用法:表单组件引用
import MyForm from './MyForm.vue'

const formRef = ref<InstanceType<typeof MyForm>>()

// 调用子组件暴露的方法
async function handleSubmit() {
  const valid = await formRef.value?.validate()
  if (valid) {
    // 提交逻辑
  }
}

个人学习笔记,部分内容借助 AI 辅助整理,仅供查阅参考,请以官方文档为准