针对 Vue 3 项目,TypeScript 的最佳实践会结合 Vue 的 Composition API 和响应式系统的特点,形成一套独特的规范体系。以下是专门为 Vue 3 + TypeScript 项目整理的实践指南:
🎯 项目初始化与配置
使用官方脚手架创建项目
npm create vue@latest
# 或
npm create vite@latest
在创建时务必勾选 TypeScript 选项,确保获得最佳的项目模板和配置。
环境类型声明
创建 src/env.d.ts 或扩充现有声明文件:
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
// 环境变量类型声明
interface ImportMetaEnv {
readonly VITE_APP_TITLE: string
readonly VITE_API_BASE_URL: string
// 更多环境变量...
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
📦 组件开发规范
1. 使用 <script setup> 语法
这是 Vue 3 推荐的组合式 API 写法,配合 TypeScript 体验最佳:
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
// 通过泛型定义 ref 类型
const count = ref<number>(0)
const user = ref<User | null>(null)
// 类型安全的计算属性
const doubleCount = computed<number>(() => count.value * 2)
// 事件声明
const emit = defineEmits<{
(e: 'change', value: number): void
(e: 'update', id: string): void
}>()
// Props 定义 - 使用类型字面量
interface Props {
title: string
items?: string[]
disabled?: boolean
config?: {
theme: 'light' | 'dark'
size: 'small' | 'medium' | 'large'
}
}
const props = withDefaults(defineProps<Props>(), {
items: () => [],
disabled: false,
config: () => ({ theme: 'light', size: 'medium' })
})
// 暴露给父组件的属性和方法
defineExpose<{
reset: () => void
validate: () => boolean
}>({
reset: () => { count.value = 0 },
validate: () => count.value > 0
})
</script>
2. 组件 Props 的严格定义
为组件 Props 定义精确的类型,避免使用过于宽泛的类型:
// ✅ 好的做法
interface TableColumn {
key: string
title: string
width?: number | string
align?: 'left' | 'center' | 'right'
sortable?: boolean
}
// ❌ 避免
interface TableColumn {
key: string
title: string
width?: any
align?: string
}
3. 模板引用的类型安全
<script setup lang="ts">
import { ref, onMounted } from 'vue'
// 定义组件实例类型
const modalRef = ref<InstanceType<typeof ModalComponent> | null>(null)
// 或使用组件导出的类型
import ModalComponent, { type ModalExpose } from './ModalComponent.vue'
const modalRef = ref<ModalExpose | null>(null)
onMounted(() => {
modalRef.value?.open() // 类型安全的方法调用
})
</script>
<template>
<ModalComponent ref="modalRef" />
</template>
🔄 Composition API 最佳实践
1. 创建类型安全的组合式函数
// composables/useAsyncData.ts
import { ref, type Ref } from 'vue'
interface AsyncState<T> {
data: Ref<T | null>
loading: Ref<boolean>
error: Ref<Error | null>
execute: () => Promise<void>
}
export function useAsyncData<T>(
fetcher: () => Promise<T>
): AsyncState<T> {
const data = ref<T | null>(null) as Ref<T | null>
const loading = ref(false)
const error = ref<Error | null>(null)
const execute = async () => {
loading.value = true
error.value = null
try {
data.value = await fetcher()
} catch (e) {
error.value = e as Error
} finally {
loading.value = false
}
}
return {
data,
loading,
error,
execute
}
}
// 使用示例
interface User {
id: number
name: string
email: string
}
const { data, loading, error } = useAsyncData<User>(
() => fetch('/api/user').then(res => res.json())
)
2. 类型安全的 Pinia Store
// stores/user.ts
import { defineStore } from 'pinia'
interface UserState {
profile: User | null
permissions: string[]
lastLogin: Date | null
}
interface UserActions {
login: (credentials: Credentials) => Promise<void>
logout: () => void
updateProfile: (data: Partial<User>) => Promise<User>
}
interface UserGetters {
isAdmin: (state: UserState) => boolean
fullName: (state: UserState) => string
}
export const useUserStore = defineStore<'user', UserState, UserGetters, UserActions>('user', {
state: (): UserState => ({
profile: null,
permissions: [],
lastLogin: null
}),
getters: {
isAdmin: (state) => state.permissions.includes('admin'),
fullName: (state) => state.profile ?
`${state.profile.firstName} ${state.profile.lastName}` : ''
},
actions: {
async login(credentials: Credentials) {
// 类型安全的登录逻辑
},
logout() {
this.$patch({
profile: null,
permissions: [],
lastLogin: null
})
},
async updateProfile(data: Partial<User>) {
// 部分更新,类型安全
}
}
})
// 在组件中使用
const userStore = useUserStore()
const { profile, isAdmin } = storeToRefs(userStore)
🛣️ Vue Router 类型安全
1. 类型化路由参数
// router/types.ts
import 'vue-router'
declare module 'vue-router' {
interface RouteMeta {
requiresAuth?: boolean
roles?: Array<'admin' | 'user'>
title?: string
transition?: string
keepAlive?: boolean
}
}
// router/index.ts
export const routes = [
{
path: '/users/:id',
name: 'UserDetail',
component: () => import('@/views/UserDetail.vue'),
meta: {
requiresAuth: true,
title: '用户详情',
roles: ['admin']
} as const // 使用 const assertion 确保类型精确
}
] as const // 使路由配置成为字面量类型
2. 路由参数的类型推导
<script setup lang="ts">
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
// 使用类型守卫处理路由参数
const userId = computed(() => {
const id = route.params.id
// 路由参数可能是 string 或 string[]
return Array.isArray(id) ? id[0] : id
})
// 类型安全的导航
function navigateToUser(id: number) {
router.push({
name: 'UserDetail',
params: { id: id.toString() }
})
}
// 使用查询参数
const page = computed(() => {
const page = route.query.page
return page ? Number(page) : 1
})
</script>
📡 API 层类型安全
1. 统一的 API 响应类型
// api/types.ts
export interface ApiResponse<T = any> {
code: number
data: T
message: string
timestamp: number
}
export interface PaginatedResponse<T> {
items: T[]
total: number
page: number
pageSize: number
totalPages: number
}
export interface ApiError {
code: number
message: string
details?: Record<string, any>
}
2. 类型安全的请求函数
// api/user.ts
import axios, { type AxiosRequestConfig } from 'axios'
import type { ApiResponse, PaginatedResponse } from './types'
export interface User {
id: number
name: string
email: string
avatar?: string
role: 'admin' | 'editor' | 'viewer'
createdAt: string
}
export interface CreateUserDto {
name: string
email: string
password: string
role?: User['role']
}
export interface UpdateUserDto extends Partial<CreateUserDto> {
id: number
}
export class UserApi {
private baseUrl = '/api/users'
// 泛型方法,返回类型安全的数据
async getUsers(params?: { page?: number; pageSize?: number }): Promise<PaginatedResponse<User>> {
const response = await axios.get<ApiResponse<PaginatedResponse<User>>>(this.baseUrl, {
params
})
return response.data.data
}
async getUserById(id: number): Promise<User> {
const response = await axios.get<ApiResponse<User>>(`${this.baseUrl}/${id}`)
return response.data.data
}
async createUser(data: CreateUserDto): Promise<User> {
const response = await axios.post<ApiResponse<User>>(this.baseUrl, data)
return response.data.data
}
async updateUser(data: UpdateUserDto): Promise<User> {
const { id, ...rest } = data
const response = await axios.put<ApiResponse<User>>(`${this.baseUrl}/${id}`, rest)
return response.data.data
}
async deleteUser(id: number): Promise<void> {
await axios.delete(`${this.baseUrl}/${id}`)
}
}
export const userApi = new UserApi()
🎨 样式与 CSS Modules
类型安全的 CSS Modules
<script setup lang="ts">
// 为 CSS Modules 生成类型
import styles from './Component.module.css'
// styles 有完整的类型提示
// styles.container, styles.title, styles.highlight
</script>
<template>
<div :class="styles.container">
<h1 :class="styles.title">标题</h1>
</div>
</template>
<style module="styles">
.container { /* ... */ }
.title { /* ... */ }
</style>
🔍 类型检查与工具
1. 使用 Volar 替代 Vetur
在 VS Code 中,为 Vue 3 项目推荐使用 Volar 扩展,并启用 Takeover Mode 以获得最佳的类型支持。
在 .vscode/settings.json 中配置:
{
"typescript.tsdk": "node_modules/typescript/lib",
"volar.takeoverMode.enabled": true,
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
}
}
2. 在 package.json 中添加类型检查脚本
{
"scripts": {
"type-check": "vue-tsc --noEmit",
"type-check:watch": "vue-tsc --noEmit --watch",
"build": "vue-tsc && vite build"
}
}
📝 ESLint 配置示例
// .eslintrc.cjs
module.exports = {
root: true,
extends: [
'plugin:vue/vue3-recommended',
'eslint:recommended',
'@vue/eslint-config-typescript/recommended',
'@vue/eslint-config-prettier'
],
rules: {
// Vue 特定规则
'vue/multi-word-component-names': 'error',
'vue/component-name-in-template-casing': ['error', 'PascalCase'],
'vue/define-props-declaration': ['error', 'type-based'],
'vue/define-emits-declaration': ['error', 'type-based'],
// TypeScript 规则
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_'
}],
// 禁止使用 console.log,除非是 warn/error
'no-console': ['error', { allow: ['warn', 'error'] }],
}
}
💡 项目目录结构建议
src/
├── assets/ # 静态资源
├── components/ # 公共组件
│ ├── common/ # 基础组件
│ └── layout/ # 布局组件
├── composables/ # 组合式函数
│ ├── useAuth.ts
│ ├── useAsyncData.ts
│ └── index.ts # 统一导出
├── views/ # 页面组件
│ ├── Home/
│ │ ├── index.vue
│ │ ├── components/ # 页面专属组件
│ │ └── types.ts # 页面专属类型
│ └── User/
├── router/ # 路由配置
│ ├── index.ts
│ ├── routes.ts
│ └── guards.ts
├── stores/ # Pinia 状态管理
│ ├── user.ts
│ └── app.ts
├── api/ # API 请求
│ ├── client.ts # axios 实例配置
│ ├── types.ts # API 公共类型
│ └── modules/ # 按模块划分
│ ├── user.ts
│ └── product.ts
├── types/ # 全局类型定义
│ ├── global.d.ts
│ ├── env.d.ts
│ └── models/ # 领域模型类型
├── utils/ # 工具函数
│ ├── format.ts
│ └── validation.ts
├── styles/ # 全局样式
└── main.ts # 入口文件
🔧 进阶类型技巧
1. 为 provide/inject 提供类型
// types/context.ts
import type { InjectionKey, Ref } from 'vue'
export interface ThemeContext {
theme: Ref<'light' | 'dark'>
toggleTheme: () => void
}
export const themeKey: InjectionKey<ThemeContext> = Symbol('theme')
// 父组件提供
const theme = ref<'light' | 'dark'>('light')
provide(themeKey, {
theme,
toggleTheme: () => theme.value = theme.value === 'light' ? 'dark' : 'light'
})
// 子组件注入
const themeContext = inject(themeKey)
if (themeContext) {
// themeContext 有完整的类型提示
console.log(themeContext.theme.value)
}
2. 泛型组件
<!-- components/List.vue -->
<script setup lang="ts" generic="T extends { id: string | number }">
defineProps<{
items: T[]
renderItem: (item: T) => any
keyExtractor?: (item: T) => string
}>()
</script>
<template>
<div class="list">
<div v-for="item in items" :key="keyExtractor?.(item) ?? item.id">
<slot name="item" :item="item">
{{ renderItem?.(item) }}
</slot>
</div>
</div>
</template>
<!-- 使用示例 -->
<script setup lang="ts">
interface User {
id: number
name: string
}
const users = ref<User[]>([/* ... */])
</script>
<template>
<List :items="users" :keyExtractor="(user) => `user-${user.id}`">
<template #item="{ item }">
{{ item.name }}
</template>
</List>
</template>
遵循这些实践,可以让你的 Vue 3 + TypeScript 项目更加健壮、可维护,同时提供优秀的开发体验。建议从项目一开始就建立这些规范,并在团队中推广使用。