Skip to content

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