Vuex与TypeScript #
类型定义 #
根状态类型 #
typescript
// types/store.ts
// 用户类型
interface User {
id: number
name: string
email: string
role: 'admin' | 'user' | 'guest'
}
// 购物车项类型
interface CartItem {
productId: number
quantity: number
price: number
}
// 根状态类型
interface RootState {
user: UserState
cart: CartState
products: ProductsState
}
// 用户模块状态
interface UserState {
profile: User | null
token: string | null
loading: boolean
error: string | null
}
// 购物车模块状态
interface CartState {
items: CartItem[]
loading: boolean
}
// 产品模块状态
interface ProductsState {
byId: Record<number, Product>
allIds: number[]
loading: boolean
}
export type {
RootState,
UserState,
CartState,
ProductsState,
User,
CartItem
}
类型安全的 Store #
创建类型化 Store #
typescript
// store/index.ts
import { createStore, Store } from 'vuex'
import { RootState } from '@/types/store'
import user from './modules/user'
import cart from './modules/cart'
const store = createStore<RootState>({
modules: {
user,
cart
},
strict: process.env.NODE_ENV !== 'production'
})
export default store
// 扩展 Vue 类型
declare module '@vue/runtime-core' {
interface ComponentCustomProperties {
$store: Store<RootState>
}
}
类型化模块 #
用户模块 #
typescript
// store/modules/user.ts
import { Module, ActionTree, MutationTree, GetterTree } from 'vuex'
import { RootState, UserState, User } from '@/types/store'
// 状态
const state: () => UserState = () => ({
profile: null,
token: null,
loading: false,
error: null
})
// Mutation 类型
interface Mutations {
SET_PROFILE(state: UserState, profile: User | null): void
SET_TOKEN(state: UserState, token: string | null): void
SET_LOADING(state: UserState, loading: boolean): void
SET_ERROR(state: UserState, error: string | null): void
}
// Mutations
const mutations: MutationTree<UserState> & Mutations = {
SET_PROFILE(state, profile) {
state.profile = profile
},
SET_TOKEN(state, token) {
state.token = token
},
SET_LOADING(state, loading) {
state.loading = loading
},
SET_ERROR(state, error) {
state.error = error
}
}
// Actions
interface LoginCredentials {
username: string
password: string
}
const actions: ActionTree<UserState, RootState> = {
async login({ commit }, credentials: LoginCredentials): Promise<User> {
commit('SET_LOADING', true)
commit('SET_ERROR', null)
try {
const { user, token } = await api.login(credentials)
commit('SET_PROFILE', user)
commit('SET_TOKEN', token)
return user
} catch (error) {
const message = error instanceof Error ? error.message : 'Login failed'
commit('SET_ERROR', message)
throw error
} finally {
commit('SET_LOADING', false)
}
},
logout({ commit }): void {
commit('SET_PROFILE', null)
commit('SET_TOKEN', null)
}
}
// Getters
const getters: GetterTree<UserState, RootState> = {
isLoggedIn: (state): boolean => !!state.token,
userName: (state): string => state.profile?.name ?? 'Guest',
userEmail: (state): string => state.profile?.email ?? ''
}
// 导出模块
const userModule: Module<UserState, RootState> = {
namespaced: true,
state,
mutations,
actions,
getters
}
export default userModule
购物车模块 #
typescript
// store/modules/cart.ts
import { Module, ActionTree, MutationTree, GetterTree } from 'vuex'
import { RootState, CartState, CartItem } from '@/types/store'
const state: () => CartState = () => ({
items: [],
loading: false
})
interface Mutations {
ADD_ITEM(state: CartState, item: CartItem): void
REMOVE_ITEM(state: CartState, productId: number): void
UPDATE_QUANTITY(state: CartState, payload: { productId: number; quantity: number }): void
CLEAR_CART(state: CartState): void
}
const mutations: MutationTree<CartState> & Mutations = {
ADD_ITEM(state, item) {
const existing = state.items.find(i => i.productId === item.productId)
if (existing) {
existing.quantity += item.quantity
} else {
state.items.push(item)
}
},
REMOVE_ITEM(state, productId) {
state.items = state.items.filter(i => i.productId !== productId)
},
UPDATE_QUANTITY(state, { productId, quantity }) {
const item = state.items.find(i => i.productId === productId)
if (item) {
item.quantity = quantity
}
},
CLEAR_CART(state) {
state.items = []
}
}
const actions: ActionTree<CartState, RootState> = {
async checkout({ state, commit }): Promise<void> {
// ...
}
}
const getters: GetterTree<CartState, RootState> = {
itemCount: (state): number =>
state.items.reduce((sum, item) => sum + item.quantity, 0),
total: (state): number =>
state.items.reduce((sum, item) => sum + item.price * item.quantity, 0),
isEmpty: (state): boolean => state.items.length === 0
}
const cartModule: Module<CartState, RootState> = {
namespaced: true,
state,
mutations,
actions,
getters
}
export default cartModule
组合式函数类型化 #
typescript
// composables/useUserStore.ts
import { computed, ComputedRef } from 'vue'
import { useStore } from 'vuex'
import { RootState, User } from '@/types/store'
interface UseUserStore {
// State
profile: ComputedRef<User | null>
token: ComputedRef<string | null>
loading: ComputedRef<boolean>
error: ComputedRef<string | null>
// Getters
isLoggedIn: ComputedRef<boolean>
userName: ComputedRef<string>
// Actions
login: (credentials: LoginCredentials) => Promise<User>
logout: () => void
}
interface LoginCredentials {
username: string
password: string
}
export function useUserStore(): UseUserStore {
const store = useStore<RootState>()
return {
// State
profile: computed(() => store.state.user.profile),
token: computed(() => store.state.user.token),
loading: computed(() => store.state.user.loading),
error: computed(() => store.state.user.error),
// Getters
isLoggedIn: computed(() => store.getters['user/isLoggedIn']),
userName: computed(() => store.getters['user/userName']),
// Actions
login: async (credentials: LoginCredentials) => {
return await store.dispatch('user/login', credentials)
},
logout: () => {
store.commit('user/SET_PROFILE', null)
store.commit('user/SET_TOKEN', null)
}
}
}
辅助函数类型化 #
typescript
// utils/store-helpers.ts
import { computed, ComputedRef } from 'vue'
import { useStore } from 'vuex'
import { RootState } from '@/types/store'
// 类型化的 mapState
export function useMapState<T extends keyof RootState>(
keys: T[]
): Record<T, ComputedRef<RootState[T]>> {
const store = useStore<RootState>()
const result = {} as Record<T, ComputedRef<RootState[T]>>
keys.forEach(key => {
result[key] = computed(() => store.state[key])
})
return result
}
// 类型化的 mapGetters
export function useMapGetters<K extends string>(
getters: Record<K, string>
): Record<K, ComputedRef<unknown>> {
const store = useStore()
const result = {} as Record<K, ComputedRef<unknown>>
Object.entries(getters).forEach(([key, getterPath]) => {
result[key as K] = computed(() => store.getters[getterPath])
})
return result
}
使用示例 #
在组件中使用 #
vue
<template>
<div>
<p v-if="isLoggedIn">Welcome, {{ userName }}</p>
<form v-else @submit.prevent="handleLogin">
<input v-model="form.username" />
<input v-model="form.password" type="password" />
<button type="submit" :disabled="loading">
{{ loading ? 'Logging in...' : 'Login' }}
</button>
</form>
<p v-if="error" class="error">{{ error }}</p>
</div>
</template>
<script lang="ts">
import { defineComponent, reactive } from 'vue'
import { useUserStore } from '@/composables/useUserStore'
export default defineComponent({
setup() {
const {
profile,
isLoggedIn,
userName,
loading,
error,
login
} = useUserStore()
const form = reactive({
username: '',
password: ''
})
const handleLogin = async () => {
try {
await login(form)
} catch (e) {
// Error handled in store
}
}
return {
profile,
isLoggedIn,
userName,
loading,
error,
form,
handleLogin
}
}
})
</script>
最佳实践 #
1. 定义完整类型 #
typescript
// 推荐:定义完整的类型
interface UserState {
profile: User | null
token: string | null
loading: boolean
error: string | null
}
// 不推荐:使用 any
interface UserState {
profile: any
token: any
}
2. 使用类型断言 #
typescript
// 在必要时使用类型断言
const user = store.state.user.profile as User
3. 导出类型 #
typescript
// types/store.ts
export type { RootState, User, CartItem }
总结 #
TypeScript 集成要点:
| 要点 | 说明 |
|---|---|
| 状态类型 | 定义 RootState 和模块状态类型 |
| 模块类型 | 使用 Module、MutationTree 等类型 |
| 组合式函数 | 返回类型化的 ComputedRef |
| 类型扩展 | 扩展 ComponentCustomProperties |
继续学习 测试策略,了解 Vuex 测试方法。
最后更新:2026-03-28