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