Vue + TypeScript 深度实战:组件类型、Props 泛型、Pinia 类型安全与 API 契约

Vue 3 与 TypeScript 结合的生产级实践:组件 Props 类型标注、defineProps 泛型、 emits 类型、ref/reactive 类型推断、computed 类型、Pinia Store 类型、Vue Router 类型安全、Zod API 数据契约、以及 strict tsconfig 配置。

Vue 3 原生支持 TypeScript<script setup lang="ts"> 让 TypeScript 成为 Vue 组件的一等公民。本文覆盖从 Props 类型定义到 Pinia Store 类型安全的完整实践。


一、组件 Props 类型定义

1.1 defineProps(推荐写法)

<script setup lang="ts">
interface Props {
  title: string
  count?: number
  items: Item[]
  onUpdate?: (id: string) => void
}

// 运行时 + 编译时类型
const props = defineProps<Props>()

// 带默认值(使用 withDefaults)
const props = withDefaults(defineProps<Props>(), {
  count: 0,
  onUpdate: () => {},
})
</script>

1.2 运行时 Props(需要运行时验证时)

<script setup lang="ts">
const props = defineProps({
  title: { type: String, required: true },
  count: { type: Number, default: 0 },
  items: { type: Array as PropType<Item[]>, required: true },
})
</script>

1.3 defineEmits 类型化

<script setup lang="ts">
const emit = defineEmits<{
  update: [value: string]           // 事件名 + 参数类型
  delete: [id: number]
  'update:modelValue': [val: boolean]  // v-model 事件
}>()

// 使用
emit('update', 'new value')
</script>

二、响应式类型

2.1 ref 类型

import { ref } from 'vue'

const count = ref<number>(0)           // Ref<number>
const user = ref<User | null>(null)     // Ref<User | null>
const list = ref<string[]>([])          // Ref<string[]>

// 复杂对象
interface State {
  loading: boolean
  data: Data | null
  error: Error | null
}
const state = ref<State>({
  loading: false,
  data: null,
  error: null,
})

2.2 reactive 类型

interface FormState {
  email: string
  password: string
  errors: Record<string, string>
}

const form = reactive<FormState>({
  email: '',
  password: '',
  errors: {},
})

// form.email 类型为 string
// form.errors 类型为 Record<string, string>

2.3 computed 类型

const user = ref<User | null>(null)

// 类型自动推断为 string | undefined
const displayName = computed(() => user.value?.name?.toUpperCase())

// 显式声明返回类型
const userList = computed<User[]>(() => {
  return userStore.users.filter(u => u.active)
})

三、Pinia 类型安全

// stores/auth.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

interface User {
  id: string
  name: string
  email: string
  role: 'admin' | 'user' | 'guest'
}

export const useAuthStore = defineStore('auth', () => {
  const user = ref<User | null>(null)
  const token = ref<string>('')

  const isLoggedIn = computed(() => !!user.value)
  const isAdmin = computed(() => user.value?.role === 'admin')

  async function login(email: string, password: string): Promise<void> {
    const res = await api.login({ email, password })
    user.value = res.user
    token.value = res.token
  }

  function logout(): void {
    user.value = null
    token.value = ''
  }

  return { user, token, isLoggedIn, isAdmin, login, logout }
})

// 使用时类型自动推导
const auth = useAuthStore()
auth.user?.name  // string | undefined
auth.login('a@b.com', 'pwd')  // 参数类型检查

四、API 数据契约(Zod)

// schemas/user.ts
import { z } from 'zod'

export const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1),
  email: z.string().email(),
  role: z.enum(['admin', 'user', 'guest']),
  createdAt: z.string().datetime(),
})

export type User = z.infer<typeof UserSchema>

// API 层
export async function fetchUser(id: string): Promise<User> {
  const res = await fetch(`/api/users/${id}`)
  const json = await res.json()
  return UserSchema.parse(json)  // 运行时验证
}

五、tsconfig.json 推荐配置

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "jsx": "preserve",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["src/**/*.ts", "src/**/*.vue"]
}

常见问题(FAQ)

<script setup lang="ts"> 报错找不到模块?

确保 env.d.tsshims-vue.d.ts 中有:

declare module '*.vue' {
  import type { DefineComponent } from 'vue'
  const component: DefineComponent<{}, {}, any>
  export default component
}

Props 解构如何保持响应性?

<script setup lang="ts">
const { title, count = 0 } = defineProps<Props>()  // Vue 3.4+ 支持解构
// title 和 count 是响应式的(编译时转换)
</script>

相关阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「frontend」更多文章