第9章 TypeScript集成
第26节 TypeScript与组合式API
9.26.1 类型化的ref和reactive
在Vue 3的组合式API中,我们使用ref和reactive来创建响应式数据。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支持会自动推断ref和reactive的类型,我们可以省略显式的类型注解:
// 自动推断类型为 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中,我们使用defineProps和defineEmits来定义组件的props和事件。TypeScript可以帮助我们为这些props和事件添加类型注解。
类型化的Props
使用<script setup lang="ts">时,我们可以通过泛型参数为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>最佳实践与注意事项
使用接口定义复杂类型:
- 对于复杂的数据结构,使用接口可以提高代码的可读性和可维护性
- 接口可以在多个组件和组合式函数中复用
合理使用泛型:
- 对于需要处理多种类型数据的组合式函数,使用泛型可以提高其复用性
- 泛型可以帮助TypeScript进行更准确的类型推断
避免过度使用any:
any类型会失去TypeScript的类型检查优势- 对于未知类型,可以使用
unknown并结合类型守卫
为组合式函数添加JSDoc注释:
- 为组合式函数添加JSDoc注释可以提高其可用性
- 注释应包含函数的用途、参数、返回值等信息
使用类型断言谨慎:
- 类型断言会绕过TypeScript的类型检查,应谨慎使用
- 优先使用类型守卫和类型推断
为组件添加完整的类型注解:
- 为props和emit添加完整的类型注解
- 使用
withDefaults为props添加默认值
小结
本节我们学习了TypeScript与组合式API的结合使用,包括:
- 类型化的ref和reactive
- 类型化的Props和Emit
- 类型化的组合式函数
通过为组合式API添加类型注解,我们可以获得更好的开发体验和代码安全性。TypeScript的静态类型检查可以帮助我们在开发阶段发现潜在错误,提高代码质量和可维护性。
思考与练习
- 创建一个类型化的ref,包含字符串、数字、布尔值等基本类型。
- 创建一个使用接口定义的reactive对象,包含嵌套结构。
- 编写一个带有类型化Props和Emit的组件。
- 创建一个泛型组合式函数,用于处理不同类型的数组。
- 编写一个异步组合式函数,用于获取API数据。
- 尝试在组件中使用类型守卫和类型断言。