TypeScript 使用指南
在 Vant + Vue 3 项目中实现类型安全开发的完整解决方案。
🎯 TypeScript 优势
核心优势
- 类型安全:编译时错误检查,减少运行时bug
- 开发效率:智能提示和自动补全,提升开发体验
- 重构友好:安全的代码重构,降低维护成本
- 文档化:类型即文档,提升代码可读性
🚀 快速开始
项目初始化
bash
# 使用 Vite 官方模板
npm create vue@latest my-vant-ts-app
# 交互式选择配置
✔ Project name: my-vant-ts-app
✔ Add TypeScript? Yes
✔ Add JSX Support? No
✔ Add Vue Router? Yes
✔ Add Pinia? Yes
✔ Add Vitest? Yes
✔ Add ESLint? Yes
✔ Add Prettier? Yes
# 安装 Vant 依赖
cd my-vant-ts-app
npm install vant @vant/touch-emulator
# 安装开发依赖
npm install -D @types/node unplugin-vue-components
⚙️ 核心配置
Vite 配置
typescript
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import Components from 'unplugin-vue-components/vite'
import { VantResolver } from 'unplugin-vue-components/resolvers'
import { resolve } from 'path'
export default defineConfig({
plugins: [
vue({
script: {
defineModel: true,
propsDestructure: true
}
}),
Components({
resolvers: [VantResolver()],
dts: true,
dirs: ['src/components'],
extensions: ['vue', 'ts'],
deep: true
})
],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@components': resolve(__dirname, 'src/components'),
'@utils': resolve(__dirname, 'src/utils'),
'@types': resolve(__dirname, 'src/types'),
'@api': resolve(__dirname, 'src/api')
}
},
server: {
port: 3000,
open: true,
cors: true
},
build: {
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'vant-vendor': ['vant']
}
}
}
}
})
TypeScript 配置
json
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"include": [
"env.d.ts",
"src/**/*",
"src/**/*.vue",
"tests/**/*"
],
"exclude": [
"dist",
"node_modules"
],
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"moduleResolution": "bundler",
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@utils/*": ["src/utils/*"],
"@types/*": ["src/types/*"],
"@api/*": ["src/api/*"]
},
"jsx": "preserve",
"jsxImportSource": "vue",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
环境类型声明
typescript
// env.d.ts
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
}
declare module 'vant' {
export * from 'vant/es'
}
interface ImportMetaEnv {
readonly VITE_APP_TITLE: string
readonly VITE_APP_VERSION: string
readonly VITE_API_BASE_URL: string
readonly VITE_APP_ENV: 'development' | 'production' | 'test'
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
declare global {
interface Window {
__VUE_DEVTOOLS_GLOBAL_HOOK__?: any
gtag?: (...args: any[]) => void
dataLayer?: any[]
}
interface DocumentEventMap {
'app:ready': CustomEvent<{ version: string }>
'user:login': CustomEvent<{ userId: string }>
}
}
export {}
📝 类型系统设计
基础业务类型
typescript
// types/user.ts
export interface BaseUser {
readonly id: string
name: string
email: string
avatar?: string
createdAt: Date
updatedAt: Date
}
export interface User extends BaseUser {
phone?: string
status: UserStatus
role: UserRole
profile: UserProfile
preferences: UserPreferences
}
export type UserStatus = 'active' | 'inactive' | 'pending' | 'suspended'
export type UserRole = 'admin' | 'user' | 'guest' | 'moderator'
export interface UserProfile {
firstName: string
lastName: string
bio?: string
location?: string
website?: string
socialLinks: SocialLinks
}
export interface SocialLinks {
github?: string
twitter?: string
linkedin?: string
}
export interface UserPreferences {
theme: 'light' | 'dark' | 'auto'
language: 'zh-CN' | 'en-US'
notifications: NotificationSettings
}
export interface NotificationSettings {
email: boolean
push: boolean
sms: boolean
categories: NotificationCategory[]
}
export type NotificationCategory =
| 'system'
| 'security'
| 'marketing'
| 'updates'
// 类型守卫函数
export function isValidUser(obj: any): obj is User {
return (
typeof obj === 'object' &&
typeof obj.id === 'string' &&
typeof obj.name === 'string' &&
typeof obj.email === 'string' &&
obj.createdAt instanceof Date
)
}
API 响应类型
typescript
// types/api.ts
export interface ApiResponse<T = unknown> {
readonly code: number
readonly message: string
readonly data: T
readonly timestamp: number
readonly requestId: string
}
export interface ApiError {
readonly code: number
readonly message: string
readonly details?: Record<string, any>
readonly field?: string
}
export interface PaginationParams {
page: number
pageSize: number
sortBy?: string
sortOrder?: 'asc' | 'desc'
}
export interface PaginationResponse<T> {
list: T[]
total: number
page: number
pageSize: number
totalPages: number
hasNext: boolean
hasPrev: boolean
}
export interface ListResponse<T> extends ApiResponse<PaginationResponse<T>> {}
export interface RequestConfig {
timeout?: number
retries?: number
showLoading?: boolean
showError?: boolean
errorHandler?: (error: ApiError) => void
}
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'
export type RequestParams = Record<string, any>
export type RequestData = Record<string, any> | FormData
export type ResponseInterceptor<T = any> = (
response: ApiResponse<T>
) => ApiResponse<T> | Promise<ApiResponse<T>>
组件 Props 类型
typescript
// types/components.ts
import type { VNode, Component } from 'vue'
export interface BaseComponentProps {
id?: string
class?: string | string[] | Record<string, boolean>
style?: string | Record<string, string | number>
}
export type ComponentSize = 'mini' | 'small' | 'normal' | 'large'
export type ComponentTheme =
| 'primary'
| 'success'
| 'warning'
| 'danger'
| 'default'
export interface ButtonProps extends BaseComponentProps {
type?: ComponentTheme
size?: ComponentSize
loading?: boolean
disabled?: boolean
round?: boolean
square?: boolean
icon?: string | Component
iconPosition?: 'left' | 'right'
nativeType?: 'button' | 'submit' | 'reset'
block?: boolean
text?: boolean
color?: string
loadingText?: string
loadingType?: 'circular' | 'spinner'
loadingSize?: string | number
}
export interface FormItemProps extends BaseComponentProps {
label?: string
name?: string
required?: boolean
rules?: ValidationRule[]
error?: string
labelWidth?: string | number
labelAlign?: 'left' | 'center' | 'right'
inputAlign?: 'left' | 'center' | 'right'
colon?: boolean
}
export interface ValidationRule {
required?: boolean
message?: string
pattern?: RegExp
min?: number
max?: number
len?: number
validator?: (value: any, rule: ValidationRule) => boolean | string | Promise<boolean | string>
trigger?: 'onChange' | 'onBlur' | 'onSubmit'
}
export type EventHandler<T = Event> = (event: T) => void
export type AsyncEventHandler<T = Event> = (event: T) => Promise<void>
export type SlotContent = string | number | VNode | VNode[]
export type ScopedSlot<T = any> = (props: T) => SlotContent
🎯 Vue 3 + Vant 类型实践
高级组件类型定义
vue
<!-- UserCard.vue -->
<script setup lang="ts">
import type { User, UserStatus } from '@/types/user'
import type { ComponentSize } from '@/types/components'
interface Props {
user: User
size?: ComponentSize
showActions?: boolean
showStatus?: boolean
clickable?: boolean
loading?: boolean
}
const props = withDefaults(defineProps<Props>(), {
size: 'normal',
showActions: true,
showStatus: true,
clickable: false,
loading: false
})
interface Emits {
(e: 'click', user: User): void
(e: 'edit', userId: string): void
(e: 'delete', userId: string): void
(e: 'statusChange', userId: string, status: UserStatus): void
}
const emit = defineEmits<Emits>()
const displayName = computed((): string => {
const { firstName, lastName } = props.user.profile
return `${firstName} ${lastName}`.trim() || props.user.name
})
const statusColor = computed((): string => {
const statusMap: Record<UserStatus, string> = {
active: '#07c160',
inactive: '#969799',
pending: '#ff976a',
suspended: '#ee0a24'
}
return statusMap[props.user.status]
})
const cardClass = computed(() => [
'user-card',
`user-card--${props.size}`,
{
'user-card--clickable': props.clickable,
'user-card--loading': props.loading
}
])
const handleClick = (): void => {
if (props.clickable && !props.loading) {
emit('click', props.user)
}
}
const handleEdit = (event: Event): void => {
event.stopPropagation()
emit('edit', props.user.id)
}
const handleDelete = (event: Event): void => {
event.stopPropagation()
emit('delete', props.user.id)
}
const handleStatusChange = (newStatus: UserStatus): void => {
emit('statusChange', props.user.id, newStatus)
}
defineExpose({
refresh: () => {
// 刷新用户数据的逻辑
}
})
</script>
<template>
<van-card
:class="cardClass"
:title="displayName"
:desc="user.email"
:thumb="user.avatar"
@click="handleClick"
>
<template #tags v-if="showStatus">
<van-tag
:color="statusColor"
size="small"
@click="handleStatusChange"
>
{{ user.status }}
</van-tag>
</template>
<template #footer v-if="showActions">
<div class="user-card__actions">
<van-button
size="small"
type="primary"
:loading="loading"
@click="handleEdit"
>
编辑
</van-button>
<van-button
size="small"
type="danger"
:loading="loading"
@click="handleDelete"
>
删除
</van-button>
</div>
</template>
</van-card>
</template>
<style scoped lang="scss">
.user-card {
&--clickable {
cursor: pointer;
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
}
&--loading {
opacity: 0.6;
pointer-events: none;
}
&__actions {
display: flex;
gap: 8px;
}
}
</style>
状态管理类型定义
typescript
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { User, UserStatus } from '@/types/user'
import type { ApiResponse, PaginationParams, ListResponse } from '@/types/api'
import { userApi } from '@/api/user'
interface UserState {
currentUser: User | null
users: User[]
loading: boolean
error: string | null
pagination: {
page: number
pageSize: number
total: number
}
}
export const useUserStore = defineStore('user', () => {
// 状态定义
const currentUser = ref<User | null>(null)
const users = ref<User[]>([])
const loading = ref<boolean>(false)
const error = ref<string | null>(null)
const pagination = ref({
page: 1,
pageSize: 20,
total: 0
})
// 计算属性 (Getters)
const isLoggedIn = computed((): boolean => {
return currentUser.value !== null
})
const activeUsers = computed((): User[] => {
return users.value.filter(user => user.status === 'active')
})
const userCount = computed((): number => {
return users.value.length
})
const hasNextPage = computed((): boolean => {
const { page, pageSize, total } = pagination.value
return page * pageSize < total
})
const hasPrevPage = computed((): boolean => {
return pagination.value.page > 1
})
// Actions
const fetchUsers = async (params?: PaginationParams): Promise<void> => {
try {
loading.value = true
error.value = null
const response: ListResponse<User> = await userApi.getUsers(params)
if (response.code === 200) {
users.value = response.data.list
pagination.value = {
page: response.data.page,
pageSize: response.data.pageSize,
total: response.data.total
}
} else {
throw new Error(response.message)
}
} catch (err) {
error.value = err instanceof Error ? err.message : '获取用户列表失败'
console.error('fetchUsers error:', err)
} finally {
loading.value = false
}
}
const fetchUserById = async (id: string): Promise<User | null> => {
try {
loading.value = true
const response: ApiResponse<User> = await userApi.getUserById(id)
if (response.code === 200) {
return response.data
} else {
throw new Error(response.message)
}
} catch (err) {
error.value = err instanceof Error ? err.message : '获取用户详情失败'
return null
} finally {
loading.value = false
}
}
const createUser = async (
userData: Omit<User, 'id' | 'createdAt' | 'updatedAt'>
): Promise<boolean> => {
try {
loading.value = true
const response: ApiResponse<User> = await userApi.createUser(userData)
if (response.code === 200) {
users.value.unshift(response.data)
pagination.value.total += 1
return true
} else {
throw new Error(response.message)
}
} catch (err) {
error.value = err instanceof Error ? err.message : '创建用户失败'
return false
} finally {
loading.value = false
}
}
const updateUser = async (id: string, userData: Partial<User>): Promise<boolean> => {
try {
loading.value = true
const response: ApiResponse<User> = await userApi.updateUser(id, userData)
if (response.code === 200) {
const index = users.value.findIndex(user => user.id === id)
if (index !== -1) {
users.value[index] = response.data
}
if (currentUser.value?.id === id) {
currentUser.value = response.data
}
return true
} else {
throw new Error(response.message)
}
} catch (err) {
error.value = err instanceof Error ? err.message : '更新用户失败'
return false
} finally {
loading.value = false
}
}
const deleteUser = async (id: string): Promise<boolean> => {
try {
loading.value = true
const response: ApiResponse<void> = await userApi.deleteUser(id)
if (response.code === 200) {
users.value = users.value.filter(user => user.id !== id)
pagination.value.total -= 1
return true
} else {
throw new Error(response.message)
}
} catch (err) {
error.value = err instanceof Error ? err.message : '删除用户失败'
return false
} finally {
loading.value = false
}
}
const setCurrentUser = (user: User | null): void => {
currentUser.value = user
}
const updateUserStatus = async (id: string, status: UserStatus): Promise<boolean> => {
return updateUser(id, { status })
}
const clearError = (): void => {
error.value = null
}
const reset = (): void => {
currentUser.value = null
users.value = []
loading.value = false
error.value = null
pagination.value = { page: 1, pageSize: 20, total: 0 }
}
return {
// 状态
currentUser: readonly(currentUser),
users: readonly(users),
loading: readonly(loading),
error: readonly(error),
pagination: readonly(pagination),
// 计算属性
isLoggedIn,
activeUsers,
userCount,
hasNextPage,
hasPrevPage,
// 方法
fetchUsers,
fetchUserById,
createUser,
updateUser,
deleteUser,
setCurrentUser,
updateUserStatus,
clearError,
reset
}
})
export type UserStore = ReturnType<typeof useUserStore>
🔧 API 请求类型封装
类型安全的 HTTP 客户端
typescript
// utils/request.ts
import axios, {
AxiosInstance,
AxiosRequestConfig,
AxiosResponse,
AxiosError
} from 'axios'
import type { ApiResponse, ApiError, RequestConfig } from '@/types/api'
import { showToast, showLoadingToast, closeToast } from 'vant'
interface ExtendedAxiosRequestConfig extends AxiosRequestConfig {
showLoading?: boolean
showError?: boolean
retries?: number
retryDelay?: number
}
class HttpClient {
private instance: AxiosInstance
private loadingCount = 0
constructor(baseURL: string) {
this.instance = axios.create({
baseURL,
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
})
this.setupInterceptors()
}
private setupInterceptors(): void {
// 请求拦截器
this.instance.interceptors.request.use(
(config: ExtendedAxiosRequestConfig) => {
if (config.showLoading) {
this.showLoading()
}
const token = localStorage.getItem('token')
if (token) {
config.headers = {
...config.headers,
Authorization: `Bearer ${token}`
}
}
return config
},
(error: AxiosError) => {
this.hideLoading()
return Promise.reject(error)
}
)
// 响应拦截器
this.instance.interceptors.response.use(
(response: AxiosResponse<ApiResponse>) => {
this.hideLoading()
const { data } = response
if (data.code !== 200) {
const error: ApiError = {
code: data.code,
message: data.message
}
if (response.config.showError !== false) {
showToast(error.message)
}
return Promise.reject(error)
}
return response
},
async (error: AxiosError) => {
this.hideLoading()
const config = error.config as ExtendedAxiosRequestConfig
if (config?.retries && config.retries > 0) {
config.retries -= 1
await this.delay(config.retryDelay || 1000)
return this.instance.request(config)
}
const apiError: ApiError = {
code: error.response?.status || 0,
message: this.getErrorMessage(error)
}
if (config?.showError !== false) {
showToast(apiError.message)
}
return Promise.reject(apiError)
}
)
}
private showLoading(): void {
if (this.loadingCount === 0) {
showLoadingToast({
message: '加载中...',
forbidClick: true,
duration: 0
})
}
this.loadingCount++
}
private hideLoading(): void {
this.loadingCount--
if (this.loadingCount <= 0) {
this.loadingCount = 0
closeToast()
}
}
private getErrorMessage(error: AxiosError): string {
if (error.response) {
const status = error.response.status
const statusMessages: Record<number, string> = {
400: '请求参数错误',
401: '未授权,请重新登录',
403: '拒绝访问',
404: '请求的资源不存在',
408: '请求超时',
500: '服务器内部错误',
502: '网关错误',
503: '服务不可用',
504: '网关超时'
}
return statusMessages[status] || `请求失败 (${status})`
} else if (error.request) {
return '网络连接失败,请检查网络'
} else {
return error.message || '未知错误'
}
}
private delay(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms))
}
async request<T = any>(config: ExtendedAxiosRequestConfig): Promise<ApiResponse<T>> {
const response = await this.instance.request<ApiResponse<T>>(config)
return response.data
}
async get<T = any>(
url: string,
params?: any,
config?: RequestConfig
): Promise<ApiResponse<T>> {
return this.request<T>({
method: 'GET',
url,
params,
...config
})
}
async post<T = any>(
url: string,
data?: any,
config?: RequestConfig
): Promise<ApiResponse<T>> {
return this.request<T>({
method: 'POST',
url,
data,
...config
})
}
async put<T = any>(
url: string,
data?: any,
config?: RequestConfig
): Promise<ApiResponse<T>> {
return this.request<T>({
method: 'PUT',
url,
data,
...config
})
}
async delete<T = any>(
url: string,
config?: RequestConfig
): Promise<ApiResponse<T>> {
return this.request<T>({
method: 'DELETE',
url,
...config
})
}
}
const httpClient = new HttpClient(import.meta.env.VITE_API_BASE_URL)
export default httpClient
📚 最佳实践总结
类型安全检查清单
- ✅ 启用 strict: true 和相关严格检查
- ✅ 使用 import type 导入类型
- ✅ 合理使用泛型和类型约束
- ✅ 实现运行时类型检查函数
- ✅ 使用 readonly 保护不可变数据
- ✅ 使用联合类型替代 any
- ✅ 善用 Partial、Pick、Omit 等工具类型
- ✅ 谨慎使用类型断言,优先类型守卫
性能优化建议
编译性能
- 使用 skipLibCheck 跳过库类型检查
- 合理配置 include/exclude
- 使用增量编译
- 避免过深的类型嵌套
打包优化
- 类型文件不会影响运行时
- 使用 Tree Shaking 移除未使用代码
- 合理分包避免类型污染
- 生产环境移除类型检查
🔗 相关资源
- TypeScript 官方文档
- Vue 3 TypeScript 指南
- Vant TypeScript 支持
- Pinia TypeScript 指南
- Vue Test Utils 类型
- Vitest TypeScript 配置
通过以上 TypeScript 配置和实践,你可以在 Vant + Vue 3 项目中享受完整的类型安全开发体验。