第9章 TypeScript集成

第26节 TypeScript与组合式API

9.26.1 类型化的ref和reactive

在Vue 3的组合式API中,我们使用refreactive来创建响应式数据。TypeScript可以帮助我们为这些响应式数据添加类型注解,提高代码的安全性和可维护性。

类型化的ref

ref用于创建基本类型的响应式数据,如字符串、数字、布尔值等。我们可以通过泛型参数为ref指定类型:

import { ref } from 'vue'

// 基本类型
const count = ref<number>(0)
const name = ref<string>('张三')
const isActive = ref<boolean>(true)
const message = ref<string | null>(null)

// 数组类型
const numbers = ref<number[]>([1, 2, 3, 4, 5])
const users = ref<string[]>(['张三', '李四', '王五'])

// 自定义类型数组
interface User {
  id: number
  name: string
  email: string
}

const userList = ref<User[]>([
  { id: 1, name: '张三', email: 'zhangsan@example.com' },
  { id: 2, name: '李四', email: 'lisi@example.com' }
])

// 使用as const断言
const readonlyRef = ref("不可修改") as const
// readonlyRef.value = "修改" // 类型错误:无法分配到 "value" ,因为它是只读属性

类型化的reactive

reactive用于创建对象类型的响应式数据。我们可以通过泛型参数或接口为reactive指定类型:

import { reactive } from 'vue'

// 使用接口
interface User {
  id: number
  name: string
  email: string
  age?: number
  isActive: boolean
}

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

// 使用内联类型
const state = reactive<{
  count: number
  message: string
  items: string[]
}>({
  count: 0,
  message: 'Hello Vue',
  items: ['item1', 'item2', 'item3']
})

// 嵌套对象类型
interface Address {
  city: string
  street: string
  zipCode: string
}

interface Person {
  name: string
  age: number
  address: Address
}

const person = reactive<Person>({
  name: '张三',
  age: 30,
  address: {
    city: '北京',
    street: '朝阳区',
    zipCode: '100000'
  }
})

ref vs reactive的类型推断

Vue 3的TypeScript支持会自动推断refreactive的类型,我们可以省略显式的类型注解:

// 自动推断类型为 ref<number>
const count = ref(0)

// 自动推断类型为 ref<string>
const name = ref('张三')

// 自动推断类型为 reactive<{ count: number; name: string }>
const state = reactive({
  count: 0,
  name: '张三'
})

但是,对于复杂类型或需要限制类型的情况,显式的类型注解仍然是推荐的:

// 自动推断为 ref<(string | number)[]>
const items = ref(['item1', 2, 'item3'])

// 显式类型注解,限制为 string[]
const stringItems = ref<string[]>(['item1', 'item2', 'item3'])

类型转换与断言

在某些情况下,我们可能需要对响应式数据进行类型转换:

import { ref, reactive } from 'vue'

// 使用类型断言
const count = ref(0) as ref<number>

// 使用as const创建只读引用
const readonlyState = reactive({
  count: 0,
  name: '张三'
}) as const

// 使用类型守卫
function isUser(obj: any): obj is User {
  return obj && typeof obj.id === 'number' && typeof obj.name === 'string'
}

const unknownRef = ref<any>({ id: 1, name: '张三' })
if (isUser(unknownRef.value)) {
  // 此时unknownRef.value被推断为User类型
  console.log(unknownRef.value.email)
}

9.26.2 类型化的Props和Emit

在组合式API中,我们使用definePropsdefineEmits来定义组件的props和事件。TypeScript可以帮助我们为这些props和事件添加类型注解。

类型化的Props

使用&lt;script setup lang=&quot;ts&quot;&gt;时,我们可以通过泛型参数为defineProps指定类型:

<template>
  <div>
    <h1>{{ title }}</h1>
    <p>{{ content }}</p>
    <button v-if="showButton" @click="onClick">点击我</button>
  </div>
</template>

<script setup lang="ts">
// 使用接口定义Props
interface Props {
  // 必填属性
  title: string
  
  // 可选属性
  content?: string
  
  // 带默认值的可选属性
  showButton?: boolean
  
  // 联合类型
  type?: 'primary' | 'secondary' | 'success' | 'danger'
  
  // 数字类型
  count?: number
}

// 定义Props
const props = defineProps<Props>()

// 定义点击事件
const emit = defineEmits<{
  (e: 'click'): void
}>()

const onClick = () => {
  emit('click')
}
</script>

带默认值的Props

在TypeScript中,我们可以使用withDefaults函数为Props添加默认值:

<script setup lang="ts">
interface Props {
  title: string
  content?: string
  showButton?: boolean
  type?: 'primary' | 'secondary' | 'success' | 'danger'
  count?: number
}

// 为Props添加默认值
const props = withDefaults(defineProps<Props>(), {
  content: '默认内容',
  showButton: true,
  type: 'primary',
  count: 0
})

// 使用默认值
console.log(props.content) // '默认内容'
console.log(props.showButton) // true
console.log(props.type) // 'primary'
console.log(props.count) // 0
</script>

类型化的Emit

使用defineEmits可以为组件的事件添加类型注解:

<template>
  <div>
    <input v-model="inputValue" placeholder="输入内容" />
    <button @click="handleSubmit">提交</button>
    <button @click="handleDelete">删除</button>
  </div>
</template>

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

const inputValue = ref('')

// 定义事件
const emit = defineEmits<{
  // 无参数事件
  (e: 'delete'): void
  
  // 带参数事件
  (e: 'submit', value: string): void
  
  // 带多个参数事件
  (e: 'update', id: number, value: string): void
  
  // 泛型事件
  <T>(e: 'change', value: T): void
}>()

const handleSubmit = () => {
  emit('submit', inputValue.value)
}

const handleDelete = () => {
  emit('delete')
}

// 泛型事件示例
const handleChange = <T>(value: T) => {
  emit('change', value)
}
</script>

在父组件中使用类型化的组件

当子组件使用TypeScript定义了props和事件后,父组件在使用该组件时会获得完整的类型提示:

<template>
  <div>
    <CustomComponent
      title="组件标题"
      content="组件内容"
      type="success"
      @submit="handleSubmit"
      @delete="handleDelete"
    />
  </div>
</template>

<script setup lang="ts">
import CustomComponent from './CustomComponent.vue'

const handleSubmit = (value: string) => {
  console.log('提交的值:', value)
}

const handleDelete = () => {
  console.log('删除事件触发')
}
</script>

9.26.3 类型化的组合式函数

组合式函数(Composables)是Vue 3中用于逻辑复用的重要机制。TypeScript可以帮助我们为组合式函数添加类型注解,提高其可用性和可维护性。

基本类型化组合式函数

// composables/useCounter.ts
import { ref, computed } from 'vue'

export function useCounter(initialValue: number = 0) {
  const count = ref(initialValue)
  
  const doubleCount = computed(() => count.value * 2)
  
  const increment = () => {
    count.value++
  }
  
  const decrement = () => {
    count.value--
  }
  
  const reset = () => {
    count.value = initialValue
  }
  
  return {
    count,
    doubleCount,
    increment,
    decrement,
    reset
  }
}

在组件中使用:

<template>
  <div>
    <h1>计数器</h1>
    <p>当前值: {{ count }}</p>
    <p>双倍值: {{ doubleCount }}</p>
    <button @click="increment">+</button>
    <button @click="decrement">-</button>
    <button @click="reset">重置</button>
  </div>
</template>

<script setup lang="ts">
import { useCounter } from '../composables/useCounter'

// 使用默认初始值
const { count, doubleCount, increment, decrement, reset } = useCounter()

// 使用自定义初始值
const { 
  count: customCount, 
  doubleCount: customDoubleCount 
} = useCounter(10)
</script>

泛型组合式函数

对于需要处理不同类型数据的组合式函数,我们可以使用泛型:

// composables/useArray.ts
import { ref } from 'vue'

export function useArray<T>(initialValue: T[] = []) {
  const items = ref<T[]>(initialValue)
  
  const add = (item: T) => {
    items.value.push(item)
  }
  
  const remove = (index: number) => {
    items.value.splice(index, 1)
  }
  
  const update = (index: number, item: T) => {
    items.value[index] = item
  }
  
  const clear = () => {
    items.value = []
  }
  
  const find = (predicate: (item: T) => boolean): T | undefined => {
    return items.value.find(predicate)
  }
  
  return {
    items,
    add,
    remove,
    update,
    clear,
    find
  }
}

在组件中使用:

<script setup lang="ts">
import { useArray } from '../composables/useArray'

// 使用字符串数组
const { items: stringItems, add: addString } = useArray<string>(['item1', 'item2'])
addString('item3')

// 使用数字数组
const { items: numberItems, add: addNumber } = useArray<number>([1, 2, 3])
addNumber(4)

// 使用自定义类型数组
interface User {
  id: number
  name: string
}

const { items: userItems, add: addUser } = useArray<User>([
  { id: 1, name: '张三' }
])

addUser({ id: 2, name: '李四' })
</script>

异步组合式函数

对于异步操作,我们可以为组合式函数添加类型注解:

// composables/useFetch.ts
import { ref, onMounted } from 'vue'

export interface FetchState<T> {
  data: T | null
  loading: boolean
  error: string | null
}

export function useFetch<T>(url: string) {
  const state = ref<FetchState<T>>({
    data: null,
    loading: false,
    error: null
  })
  
  const fetchData = async () => {
    state.value.loading = true
    state.value.error = null
    
    try {
      const response = await fetch(url)
      if (!response.ok) {
        throw new Error(`HTTP error! status: ${response.status}`)
      }
      const data = await response.json()
      state.value.data = data
    } catch (error) {
      state.value.error = error instanceof Error ? error.message : 'An unknown error occurred'
    } finally {
      state.value.loading = false
    }
  }
  
  onMounted(() => {
    fetchData()
  })
  
  return {
    ...state.value,
    fetchData
  }
}

在组件中使用:

<template>
  <div>
    <h1>用户列表</h1>
    
    <div v-if="loading">加载中...</div>
    
    <div v-else-if="error">
      <p>错误: {{ error }}</p>
      <button @click="fetchData">重试</button>
    </div>
    
    <ul v-else-if="data">
      <li v-for="user in data" :key="user.id">
        {{ user.name }} - {{ user.email }}
      </li>
    </ul>
  </div>
</template>

<script setup lang="ts">
import { useFetch } from '../composables/useFetch'

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

const { data, loading, error, fetchData } = useFetch<User[]>('https://jsonplaceholder.typicode.com/users')
</script>

最佳实践与注意事项

  1. 使用接口定义复杂类型

    • 对于复杂的数据结构,使用接口可以提高代码的可读性和可维护性
    • 接口可以在多个组件和组合式函数中复用
  2. 合理使用泛型

    • 对于需要处理多种类型数据的组合式函数,使用泛型可以提高其复用性
    • 泛型可以帮助TypeScript进行更准确的类型推断
  3. 避免过度使用any

    • any类型会失去TypeScript的类型检查优势
    • 对于未知类型,可以使用unknown并结合类型守卫
  4. 为组合式函数添加JSDoc注释

    • 为组合式函数添加JSDoc注释可以提高其可用性
    • 注释应包含函数的用途、参数、返回值等信息
  5. 使用类型断言谨慎

    • 类型断言会绕过TypeScript的类型检查,应谨慎使用
    • 优先使用类型守卫和类型推断
  6. 为组件添加完整的类型注解

    • 为props和emit添加完整的类型注解
    • 使用withDefaults为props添加默认值

小结

本节我们学习了TypeScript与组合式API的结合使用,包括:

  • 类型化的ref和reactive
  • 类型化的Props和Emit
  • 类型化的组合式函数

通过为组合式API添加类型注解,我们可以获得更好的开发体验和代码安全性。TypeScript的静态类型检查可以帮助我们在开发阶段发现潜在错误,提高代码质量和可维护性。

思考与练习

  1. 创建一个类型化的ref,包含字符串、数字、布尔值等基本类型。
  2. 创建一个使用接口定义的reactive对象,包含嵌套结构。
  3. 编写一个带有类型化Props和Emit的组件。
  4. 创建一个泛型组合式函数,用于处理不同类型的数组。
  5. 编写一个异步组合式函数,用于获取API数据。
  6. 尝试在组件中使用类型守卫和类型断言。
« 上一篇 24-typescript-basics 下一篇 » 26-typescript-advanced