Vue 3 + TypeScript 常见问题及解决
Vue 3 使用 TypeScript 时的常见问题和解决方案
组件类型定义
1. 父子组件通信类型
typescript
<script setup lang="ts">
// 定义 props 接口,包含必填和可选属性
interface Props {
title: string // 必填的标题
count?: number // 可选的数量,默认值为 0
items: string[] // 字符串数组
onUpdate: (value: string) => void // 更新回调函数类型
}
// 使用 withDefaults 设置默认值,count 默认 0
const props = withDefaults(defineProps<Props>(), {
count: 0
})
// 定义 emits 类型,限制事件参数类型
const emit = defineEmits<{
(e: 'update', value: string): void // update 事件,参数为字符串
(e: 'change', id: number): void // change 事件,参数为数字
}>()
// 在模板中使用
// {{ props.title }}
// {{ props.count }}
// emit('update', 'new value')
</script>2. ref 和 reactive 类型
typescript
<script setup lang="ts">
import { ref, reactive } from 'vue'
// ref 用于基本类型,自动推断为 Ref<T>
// 也可以显式指定类型
const count = ref<number>(0) // 数字类型
const name = ref<string>('') // 字符串类型
const loading = ref<boolean>(false) // 布尔类型
// ref 用于复杂类型,可能为 null 时加 | null
const user = ref<{ name: string; age: number } | null>(null)
// ref 用于数组
const list = ref<string[]>([])
// reactive 用于对象,会深层次响应
const form = reactive({
username: '', // 用户名
password: '', // 密码
remember: false // 是否记住
})
// reactive 用于数组
const items = reactive<string[]>([])
</script>3. 获取组件实例类型
typescript
<script setup lang="ts">
import { ref } from 'vue'
import type { ComponentPublicInstance } from 'vue'
// 获取子组件实例,使用 ComponentPublicInstance 类型
const childRef = ref<ComponentPublicInstance | null>(null)
// 调用子组件方法
function handleClick() {
childRef.value?.someMethod()
}
// 获取 DOM 元素,使用具体的 HTML 元素类型
const inputRef = ref<HTMLInputElement | null>(null)
// 调用 DOM 方法
function focusInput() {
inputRef.value?.focus()
}
</script>常见问题解决
1. defineProps 类型推断失败
typescript
// 错误写法:使用对象语法,TypeScript 无法推断类型
const props = defineProps({
name: String,
age: Number
})
// 正确写法:使用泛型,TypeScript 可以完整推断类型
const props = defineProps<{
name: string
age?: number
}>()
// 或者使用类型声明接口,代码更清晰
interface Props {
name: string
age?: number
}
const props = defineProps<Props>()2. defineEmits 类型定义
typescript
// 错误写法:使用数组语法,丢失类型信息
const emit = defineEmits(['update', 'delete'])
// 正确写法:使用类型化语法,保留参数类型
const emit = defineEmits<{
(e: 'update', value: string): void
(e: 'delete', id: number): void
}>()
// 或者使用简洁的元组语法
const emit = defineEmits<{
update: [value: string] // update 事件携带字符串参数
delete: [id: number] // delete 事件携带数字参数
}>()
// 触发事件
emit('update', 'new value')
emit('delete', 1)3. 模板中类型错误提示
typescript
<template>
<!-- 使用 v-if 类型守卫消除类型错误 -->
<div v-if="typeof user?.name === 'string'">
{{ user.name }}
</div>
</template>
<script setup lang="ts">
// user 可能为 null,需要可选链或类型守卫
const user = ref<{ name: string } | null>(null)
</script>4. 路由类型定义
typescript
import { useRouter, useRoute } from 'vue-router'
// 获取路由实例
const router = useRouter()
const route = useRoute()
// route.params 和 route.query 的类型是 Record<string, string>
// 需要使用 as 断言为具体类型
const id = route.params.id as string
const query = route.query.name as string
// 编程式导航,参数类型自动推断
router.push({
path: '/detail',
query: {
id: 1,
name: 'test'
}
})
// 扩展 RouteMeta 接口,为路由元数据添加类型
declare module 'vue-router' {
interface RouteMeta {
title?: string // 页面标题
keepAlive?: boolean // 是否缓存
}
}5. Pinia 类型定义
typescript
import { defineStore } from 'pinia'
// 定义状态接口
interface UserState {
name: string // 用户名
age: number // 年龄
token: string | null // 登录凭证
}
// 定义 Store,使用泛型
export const useUserStore = defineStore('user', {
// state 返回对象,类型为 UserState
state: (): UserState => ({
name: '',
age: 0,
token: null
}),
// getters 可以推断返回类型
getters: {
// 返回 boolean,TypeScript 自动推断
isLogin: (state) => !!state.token
},
// actions 方法
actions: {
// Partial<UserState> 表示部分可选属性
setUser(user: Partial<UserState>) {
Object.assign(this, user)
},
// 异步登录方法
async login(username: string, password: string) {
const res = await api.login(username, password)
this.token = res.data.token
}
}
})
// 使用 Store,类型自动推断
const userStore = useUserStore()
userStore.name // string 类型
userStore.isLogin // boolean 类型6. API 返回类型定义
typescript
// 定义数据接口
interface User {
id: number // 用户 ID
name: string // 用户名
avatar: string // 头像 URL
}
// 定义 API 响应接口
interface UserResponse {
code: number // 状态码
data: User[] // 用户列表
message: string // 提示信息
}
// 方法一:直接指定返回类型
async function getUserList(): Promise<UserResponse> {
const res = await axios.get<UserResponse>('/api/users')
return res.data
}
// 方法二:使用泛型,支持不同接口
async function getUserList<T>(): Promise<T> {
const res = await axios.get<T>('/api/users')
return res.data
}7. 事件处理类型
typescript
<script setup lang="ts">
// 点击事件,MouseEvent 包含点击相关信息
function handleClick(event: MouseEvent) {
console.log(event.target) // 触发事件的元素
}
// 输入事件,Event 是基础类型,需要断言为具体元素类型
function handleInput(event: Event) {
// 将 target 断言为 HTMLInputElement,获取 value
const target = event.target as HTMLInputElement
console.log(target.value)
}
// 表单提交事件
function handleSubmit(event: Event) {
event.preventDefault() // 阻止表单默认提交行为
}
</script>
<template>
<button @click="handleClick">点击</button>
<input @input="handleInput" />
<form @submit="handleSubmit"></form>
</template>8. 第三方库类型
typescript
// 安装类型定义包,-D 表示仅开发环境使用
npm install -D @types/lodash
// 使用时自动获得类型提示
import _ from 'lodash'
// 如果第三方库没有提供类型定义,可以手动声明
declare module 'some-library' {
export function someFunction(): void
}9. 泛型组件
typescript
<script setup lang="ts">
// 定义泛型接口,T 为任意类型
interface Props<T> {
list: T[] // 泛型数组
render: (item: T, index: number) => VNode // 渲染函数
}
// 使用时指定具体类型,如 string、number 或自定义类型
const props = defineProps<Props<string>>()
</script>
<template>
<!-- 遍历列表,调用渲染函数 -->
<div v-for="(item, index) in props.list" :key="index">
<component :is="props.render(item, index)" />
</div>
</template>10. 响应式丢失问题
typescript
<script setup lang="ts">
import { toRefs, toRef } from 'vue'
// 从 props 解构会丢失响应性,count 将变为普通值
const { name, age } = props
// 使用 toRefs 保持响应性,返回的是 ref 对象
const { name, age } = toRefs(props)
// 或者使用 toRef 单个转换
const name = toRef(props, 'name')11. 循环引用类型
typescript
// 相互引用的两个接口
interface TypeA {
id: number
b?: TypeB // 可选的 TypeB 属性
}
interface TypeB {
id: number
a?: TypeA // 可选的 TypeA 属性
}
// 使用时需要注意可能的 undefined
const itemA: TypeA = {
id: 1,
b: { id: 2, a: undefined }
}12. Watch 监听类型
typescript
<script setup lang="ts">
import { watch, ref } from 'vue'
const count = ref(0)
// 监听单个 ref,新值和旧值都会传入
watch(count, (newVal, oldVal) => {
console.log(newVal, oldVal)
})
// 监听多个数据,数组形式传入
watch([count, () => props.name], ([newCount, newName], [oldCount, oldName]) => {
console.log(newCount, newName)
})
// 深度监听对象内部变化
watch(() => props.obj, (newVal) => {
console.log(newVal)
}, { deep: true })
</script>常用类型速查
| 类型 | 说明 |
|---|---|
string | 字符串 |
number | 数字 |
boolean | 布尔值 |
any | 任意类型,关闭类型检查 |
void | 无返回值,常用于函数 |
never | 永不返回,如死循环或抛异常 |
unknown | 未知类型,比 any 安全 |
Array<T> | 泛型数组写法 |
T[] | 数组简写,推荐使用 |
Record<K, T> | 键值对,K 为键类型,T 为值类型 |
Partial<T> | T 的所有属性都变为可选 |
Required<T> | T 的所有属性都变为必填 |
Pick<T, K> | 从 T 中选取指定属性 K |
Omit<T, K> | 从 T 中排除指定属性 K |