55. ref与reactive的类型标注

📖 概述

在Vue 3的组合式API中,ref和reactive是创建响应式数据的核心API。正确的类型标注对于确保响应式数据的类型安全至关重要。本集将深入讲解ref与reactive的类型标注方法,包括基本类型标注、复杂类型标注、泛型标注、类型转换等,帮助你编写更加安全、可靠的Vue 3 + TypeScript代码。

✨ 核心知识点

1. ref的类型标注

基本类型标注

<script setup lang="ts">
import { ref } from 'vue'

// 自动类型推断
const count = ref(0) // 推断为Ref<number>类型
const message = ref('Hello') // 推断为Ref<string>类型
const isActive = ref(true) // 推断为Ref<boolean>类型

// 显式类型标注
const count2 = ref<number>(0) // 显式标注为Ref<number>类型
const message2 = ref<string>('Hello') // 显式标注为Ref<string>类型
const isActive2 = ref<boolean>(true) // 显式标注为Ref<boolean>类型

// 空值初始化
const count3 = ref<number | null>(null) // 标注为Ref<number | null>类型
const message3 = ref<string>('') // 标注为Ref<string>类型,初始值为空字符串

// 使用类型标注访问value
console.log(count.value.toFixed(0)) // 正确,count.value被推断为number类型
console.log(message.value.toUpperCase()) // 正确,message.value被推断为string类型
</script>

复杂类型标注

<script setup lang="ts">
import { ref } from 'vue'

// 定义接口
interface User {
  id: number
  name: string
  email: string
  avatar?: string
  createdAt: Date
}

// 对象类型标注
const user = ref<User>({
  id: 1,
  name: '张三',
  email: 'zhangsan@example.com',
  createdAt: new Date()
})

// 数组类型标注
const users = ref<User[]>([])

// 联合类型标注
const mixed = ref<string | number | boolean>('string')

// 复杂嵌套类型标注
const nested = ref<{
  user: User
  posts: Array<{
    id: number
    title: string
    content: string
  }>
}>({
  user: {
    id: 1,
    name: '张三',
    email: 'zhangsan@example.com',
    createdAt: new Date()
  },
  posts: []
})

// 使用复杂类型ref
user.value.name = '李四' // 正确,name被推断为string类型
users.value.push({
  id: 2,
  name: '王五',
  email: 'wangwu@example.com',
  createdAt: new Date()
}) // 正确,push方法接收User类型
</script>

泛型标注

<script setup lang="ts">
import { ref } from 'vue'

// 泛型函数返回ref
function createRef<T>(initialValue: T) {
  return ref<T>(initialValue)
}

// 使用泛型函数
const count = createRef<number>(0) // 推断为Ref<number>类型
const message = createRef<string>('Hello') // 推断为Ref<string>类型
const user = createRef<User>({
  id: 1,
  name: '张三',
  email: 'zhangsan@example.com',
  createdAt: new Date()
}) // 推断为Ref<User>类型

// 泛型约束
interface HasId {
  id: number
}

function createIdRef<T extends HasId>(initialValue: T) {
  return ref<T>(initialValue)
}

// 正确使用,User类型有id属性
const user2 = createIdRef<User>({
  id: 1,
  name: '张三',
  email: 'zhangsan@example.com',
  createdAt: new Date()
})

// 错误,缺少id属性
// const invalid = createIdRef({ name: '张三' }) // 类型错误
</script>

2. reactive的类型标注

基本类型标注

<script setup lang="ts">
import { reactive } from 'vue'

// 自动类型推断
const state = reactive({
  count: 0,
  message: 'Hello',
  isActive: true
}) // 推断为{ count: number; message: string; isActive: boolean }类型

// 显式类型标注
interface State {
  count: number
  message: string
  isActive: boolean
}

const state2 = reactive<State>({
  count: 0,
  message: 'Hello',
  isActive: true
})

// 使用reactive对象
state.count++ // 正确,count被推断为number类型
state.message = 'Hello Vue 3' // 正确,message被推断为string类型
</script>

复杂类型标注

<script setup lang="ts">
import { reactive } from 'vue'

// 定义接口
interface User {
  id: number
  name: string
  email: string
  avatar?: string
  createdAt: Date
}

interface Post {
  id: number
  title: string
  content: string
  author: User
  tags: string[]
  published: boolean
  createdAt: Date
}

// 复杂对象类型标注
const post = reactive<Post>({
  id: 1,
  title: 'Vue 3 TypeScript教程',
  content: '这是一篇关于Vue 3 TypeScript的教程',
  author: {
    id: 1,
    name: '张三',
    email: 'zhangsan@example.com',
    createdAt: new Date()
  },
  tags: ['vue', 'typescript', '教程'],
  published: true,
  createdAt: new Date()
})

// 数组类型标注
const posts = reactive<Post[]>([])

// 嵌套reactive对象
const appState = reactive({
  user: {
    id: 1,
    name: '张三',
    email: 'zhangsan@example.com',
    createdAt: new Date()
  },
  posts: [] as Post[],
  settings: {
    darkMode: false,
    notifications: true,
    language: 'zh-CN'
  }
})

// 使用复杂reactive对象
post.author.name = '李四' // 正确,name被推断为string类型
posts.push(post) // 正确,push方法接收Post类型
appState.settings.darkMode = true // 正确,darkMode被推断为boolean类型
</script>

只读类型标注

<script setup lang="ts">
import { reactive, readonly } from 'vue'

// 定义接口
interface Config {
  apiUrl: string
  timeout: number
  retryCount: number
}

// 只读reactive对象
const config = readonly<Config>({
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retryCount: 3
})

// 错误,只读对象不能修改
// config.apiUrl = 'https://new-api.example.com' // 类型错误

// 只读数组
const readonlyArray = readonly<string[]>(['a', 'b', 'c'])

// 错误,只读数组不能修改
// readonlyArray.push('d') // 类型错误
</script>

3. ref与reactive的类型转换

toRef与toRefs

<script setup lang="ts">
import { ref, reactive, toRef, toRefs } from 'vue'

// 定义接口
interface User {
  id: number
  name: string
  email: string
}

// reactive对象
const user = reactive<User>({
  id: 1,
  name: '张三',
  email: 'zhangsan@example.com'
})

// toRef:将reactive对象的单个属性转换为ref
const nameRef = toRef(user, 'name') // 类型为Ref<string>

// toRefs:将reactive对象的所有属性转换为ref
const userRefs = toRefs(user) // 类型为{ id: Ref<number>; name: Ref<string>; email: Ref<string> }

// 使用转换后的ref
nameRef.value = '李四' // 正确,修改nameRef会影响原始user对象
console.log(user.name) // 输出:李四

userRefs.email.value = 'lisi@example.com' // 正确,修改userRefs.email会影响原始user对象
console.log(user.email) // 输出:lisi@example.com
</script>

toRaw

<script setup lang="ts">
import { ref, reactive, toRaw } from 'vue'

// 定义接口
interface Product {
  id: string
  name: string
  price: number
}

// reactive对象
const product = reactive<Product>({
  id: '1',
  name: '产品1',
  price: 100
})

// toRaw:获取reactive对象的原始对象
const rawProduct = toRaw(product) // 类型为Product,非响应式

// 修改原始对象不会触发响应式更新
rawProduct.price = 200
console.log(product.price) // 输出:100(不会更新)

// ref对象
const count = ref<number>(0)

// toRaw:获取ref对象的原始值
const rawCount = toRaw(count.value) // 类型为number
console.log(rawCount) // 输出:0
</script>

4. 泛型在ref与reactive中的应用

泛型ref

<script setup lang="ts">
import { ref } from 'vue'

// 泛型函数:创建带有默认值的ref
function createDefaultRef<T>(defaultValue: T) {
  return ref<T>(defaultValue)
}

// 使用泛型函数
const count = createDefaultRef<number>(0) // Ref<number>
const message = createDefaultRef<string>('Hello') // Ref<string>
const isActive = createDefaultRef<boolean>(true) // Ref<boolean>

// 泛型约束:创建带有id的ref
interface WithId {
  id: number
}

function createIdRef<T extends WithId>(initialValue: T) {
  return ref<T>(initialValue)
}

// 正确使用
const userRef = createIdRef({
  id: 1,
  name: '张三',
  email: 'zhangsan@example.com'
}) // Ref<{ id: number; name: string; email: string }>

// 错误,缺少id属性
// const invalidRef = createIdRef({ name: '张三' }) // 类型错误
</script>

泛型reactive

<script setup lang="ts">
import { reactive } from 'vue'

// 泛型函数:创建带有元数据的reactive对象
function createWithMetadata<T>(data: T) {
  return reactive<{
    data: T
    metadata: {
      createdAt: Date
      updatedAt: Date
      version: number
    }
  }>({
    data,
    metadata: {
      createdAt: new Date(),
      updatedAt: new Date(),
      version: 1
    }
  })
}

// 使用泛型函数
const userWithMetadata = createWithMetadata({
  id: 1,
  name: '张三',
  email: 'zhangsan@example.com'
}) // 推断为reactive对象,data属性为用户类型

const productWithMetadata = createWithMetadata({
  id: '1',
  name: '产品1',
  price: 100
}) // 推断为reactive对象,data属性为产品类型

// 更新数据
userWithMetadata.data.name = '李四' // 正确
productWithMetadata.data.price = 200 // 正确
</script>

5. 类型断言的使用

基本类型断言

<script setup lang="ts">
import { ref, reactive } from 'vue'

// ref类型断言
const count = ref(0) as ref<number>
const message = ref('') as ref<string>

// reactive类型断言
const state = reactive({}) as {
  user: {
    id: number
    name: string
  }
  count: number
}

// 使用类型断言
state.user = {
  id: 1,
  name: '张三'
} // 正确
</script>

复杂类型断言

<script setup lang="ts">
import { ref } from 'vue'

// 从API获取数据,使用类型断言
const fetchUser = async () => {
  const response = await fetch('https://api.example.com/users/1')
  const data = await response.json()
  return data as User
}

// 使用类型断言初始化ref
const user = ref<User>(await fetchUser())

// 数组类型断言
const fetchUsers = async () => {
  const response = await fetch('https://api.example.com/users')
  const data = await response.json()
  return data as User[]
}

const users = ref<User[]>(await fetchUsers())
</script>

🚀 实战案例

1. 表单数据管理

<template>
  <div class="user-form">
    <h2>User Form</h2>
    <form @submit.prevent="submitForm">
      <div class="form-group">
        <label for="name">Name</label>
        <input
          type="text"
          id="name"
          v-model="form.name"
          placeholder="Enter your name"
        />
      </div>
      <div class="form-group">
        <label for="email">Email</label>
        <input
          type="email"
          id="email"
          v-model="form.email"
          placeholder="Enter your email"
        />
      </div>
      <div class="form-group">
        <label for="age">Age</label>
        <input
          type="number"
          id="age"
          v-model.number="form.age"
          placeholder="Enter your age"
        />
      </div>
      <div class="form-group">
        <label for="avatar">Avatar URL</label>
        <input
          type="url"
          id="avatar"
          v-model="form.avatar"
          placeholder="Enter your avatar URL"
        />
      </div>
      <button type="submit">Submit</button>
    </form>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive } from 'vue'

// 定义表单数据接口
interface FormData {
  name: string
  email: string
  age: number
  avatar?: string
}

// 使用reactive管理表单数据
const form = reactive<FormData>({
  name: '',
  email: '',
  age: 0,
  avatar: ''
})

// 使用ref管理表单状态
const isSubmitting = ref<boolean>(false)
const error = ref<string>('')
const success = ref<string>('')

// 表单提交
const submitForm = async () => {
  isSubmitting.value = true
  error.value = ''
  success.value = ''
  
  try {
    // 模拟API请求
    await new Promise(resolve => setTimeout(resolve, 1000))
    
    // 验证表单
    if (!form.name) {
      throw new Error('Name is required')
    }
    
    if (!form.email) {
      throw new Error('Email is required')
    }
    
    if (form.age < 18) {
      throw new Error('Age must be at least 18')
    }
    
    // 提交成功
    success.value = 'Form submitted successfully!'    
    
    // 重置表单
    Object.assign(form, {
      name: '',
      email: '',
      age: 0,
      avatar: ''
    })
  } catch (err) {
    error.value = (err as Error).message
  } finally {
    isSubmitting.value = false
  }
}
</script>

<style scoped>
.user-form {
  max-width: 500px;
  margin: 0 auto;
  padding: 20px;
  border: 1px solid #e0e0e0;
  border-radius: 8px;
}

.form-group {
  margin-bottom: 16px;
}

label {
  display: block;
  margin-bottom: 8px;
  font-weight: bold;
}

input {
  width: 100%;
  padding: 8px;
  border: 1px solid #e0e0e0;
  border-radius: 4px;
}

button {
  background-color: #42b883;
  color: white;
  border: none;
  padding: 8px 16px;
  border-radius: 4px;
  cursor: pointer;
}

button:disabled {
  background-color: #ccc;
  cursor: not-allowed;
}
</style>

📝 最佳实践

  1. 优先使用自动类型推断

    • TypeScript可以自动推断ref和reactive的类型
    • 减少显式类型标注,提高开发效率
    • 只在必要时使用显式类型标注
  2. 使用接口或类型别名定义复杂类型

    • 提高代码的可读性和可维护性
    • 便于复用和扩展
    • 清晰的类型定义
  3. 避免使用any类型

    • 尽量使用具体类型
    • 不确定类型时使用unknown替代any
    • 使用类型守卫处理不确定类型
  4. 合理使用泛型

    • 提高代码的复用性
    • 保持类型安全
    • 支持多种数据类型
  5. 何时使用ref,何时使用reactive

    • 基本类型:使用ref
    • 复杂对象:使用reactive
    • 需要单独传递的属性:使用ref
    • 完整的状态对象:使用reactive
  6. 使用toRefs解构reactive对象

    • 保持响应性
    • 便于在模板中使用
    • 提高代码的可读性
  7. 避免直接修改props

    • 使用emit触发事件修改父组件数据
    • 保持单向数据流
    • 提高组件的可维护性

💡 常见问题与解决方案

  1. ref.value类型推断不准确

    • 检查ref的初始值是否提供了足够的类型信息
    • 尝试添加显式类型标注
    • 检查TypeScript版本是否支持最新的类型推断特性
  2. reactive对象类型不生效

    • 确保reactive对象的初始值与类型定义一致
    • 尝试使用类型断言
    • 检查接口或类型别名定义是否正确
  3. toRefs返回的ref类型错误

    • 确保reactive对象的类型定义正确
    • 尝试添加显式类型标注
    • 检查TypeScript版本是否支持toRefs的类型推断
  4. 只读类型修改错误

    • 确保只读对象不会被修改
    • 使用readonly创建只读对象
    • 检查类型定义是否正确
  5. 泛型类型不工作

    • 确保泛型约束正确
    • 检查泛型函数的类型定义
    • 确保TypeScript版本支持泛型特性
  6. 类型断言过度使用

    • 尝试使用更精确的类型定义
    • 利用TypeScript的类型推断
    • 考虑使用类型守卫

📚 进一步学习资源

🎯 课后练习

  1. 基础练习

    • 创建不同类型的ref和reactive对象,添加正确的类型标注
    • 练习使用toRef、toRefs和toRaw进行类型转换
    • 尝试使用泛型创建可复用的ref和reactive函数
  2. 进阶练习

    • 实现一个表单数据管理系统,使用ref和reactive管理表单状态
    • 创建一个复杂的状态管理系统,包含嵌套对象和数组
    • 练习使用类型断言处理API返回数据
  3. 实战练习

    • 重构一个现有的Vue 3项目,添加正确的类型标注
    • 优化类型定义,提高类型安全性
    • 解决项目中存在的类型错误
  4. 类型系统练习

    • 实现复杂的类型定义,包括联合类型、交叉类型、条件类型等
    • 测试类型标注的边界情况
    • 优化类型定义,提高类型推断的效果

通过本集的学习,你已经掌握了Vue 3中ref与reactive的类型标注方法和最佳实践。在实际开发中,正确的类型标注可以提高代码的类型安全性、可读性和可维护性,是Vue 3 + TypeScript开发的重要技能。下一集我们将深入学习组合式函数的类型化,进一步提升Vue 3 + TypeScript的开发能力。

« 上一篇 Vue3 + TypeScript 系列教程 - 第54集:自定义事件的类型安全 下一篇 » Vue3 + TypeScript 系列教程 - 第56集:组合式函数的类型化