zzz 发表于 2025-9-26 15:01:37

Pinia 与 TypeScript 集成指南

本帖最后由 zzz 于 2025-9-26 15:11 编辑

Pinia 是 Vue.js 的官方状态管理库,与 TypeScript 的集成非常出色。下面我将详细介绍如何将两者结合使用。

1.基础设置
安装依赖
npm install pinia
npm install -D typescript @vue/tsconfig创建 Pinia Store 并添加类型// stores/counter.ts
import { defineStore } from 'pinia'

// 定义状态接口
interface CounterState {
count: number
name: string
}

export const useCounterStore = defineStore('counter', {
// 状态类型注解
state: (): CounterState => ({
    count: 0,
    name: 'Counter Store'
}),

// Getter 类型推断
getters: {
    doubleCount: (state) => state.count * 2,
    // 明确返回类型
    doubleCountPlusOne(): number {
      return this.doubleCount + 1
    },
    // 使用参数并指定返回类型
    getCountPlus: (state) => {
      return (plus: number): number => state.count + plus
    }
},

actions: {
    // Action 方法
    increment() {
      this.count++
    },
    // 带参数的 action
    incrementBy(amount: number) {
      this.count += amount
    },
    // 异步 action
    async incrementAsync() {
      const response = await fetch('/api/increment')
      const data = await response.json()
      this.count += data.amount
    }
}
})2. 组合式 API 风格的 Store// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

interface User {
id: number
name: string
email: string
}

export const useUserStore = defineStore('user', () => {
// 状态
const user = ref<User | null>(null)
const isLoggedIn = ref(false)

// Getter 等价物
const userName = computed(() => user.value?.name || 'Guest')
const userEmail = computed(() => user.value?.email || '')

// Actions
function login(email: string, password: string) {
    return new Promise<void>((resolve) => {
      setTimeout(() => {
      user.value = {
          id: 1,
          name: 'John Doe',
          email: email
      }
      isLoggedIn.value = true
      resolve()
      }, 1000)
    })
}

function logout() {
    user.value = null
    isLoggedIn.value = false
}

return {
    user,
    isLoggedIn,
    userName,
    userEmail,
    login,
    logout
}
})3. 在组件中使用
选项式API组件
<template>
<div>
<h1>{{ store.name }}</h1>
<p>Count: {{ store.count }}</p>
<p>Double: {{ store.doubleCount }}</p>
<button @click="store.increment()">Increment</button>
</div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import { useCounterStore } from '@/stores/counter'

export default defineComponent({
setup() {
const store = useCounterStore()

return {
store
}
}
})
</script>组合式API组件
<template>
<div>
<h1>User: {{ userName }}</h1>
<p>Email: {{ userEmail }}</p>
<p v-if="isLoggedIn">Status: Logged In</p>
<button @click="login('test@example.com', 'password')">Login</button>
<button @click="logout">Logout</button>
</div>
</template>

<script setup lang="ts">
import { storeToRefs } from 'pinia'
import { useUserStore } from '@/stores/user'

const userStore = useUserStore()

// 使用 storeToRefs 保持响应式
const { userName, userEmail, isLoggedIn } = storeToRefs(userStore)

// 直接解构 actions
const { login, logout } = userStore
</script>4. 高级类型技巧
自定义类型 Store// stores/types.ts
export interface Product {
id: number
name: string
price: number
category: string
}

export interface CartItem {
product: Product
quantity: number
}

export interface CartState {
items: CartItem[]
total: number
}使用复杂类型的 Store
// stores/cart.ts
import { defineStore } from 'pinia'
import type { CartState, CartItem, Product } from './types'

export const useCartStore = defineStore('cart', {
state: (): CartState => ({
    items: [],
    total: 0
}),

getters: {
    itemCount: (state) => state.items.reduce((sum, item) => sum + item.quantity, 0),
    getItemByProductId: (state) => {
      return (productId: number): CartItem | undefined =>
      state.items.find(item => item.product.id === productId)
    }
},

actions: {
    addItem(product: Product, quantity: number = 1) {
      const existingItem = this.items.find(item => item.product.id === product.id)
      
      if (existingItem) {
      existingItem.quantity += quantity
      } else {
      this.items.push({ product, quantity })
      }
      
      this.updateTotal()
    },
   
    removeItem(productId: number) {
      this.items = this.items.filter(item => item.product.id !== productId)
      this.updateTotal()
    },
   
    updateTotal() {
      this.total = this.items.reduce((sum, item) =>
      sum + (item.product.price * item.quantity), 0
      )
    }
}
})5. 插件与类型扩展
创建带类型的插件// plugins/persistence.ts
import { PiniaPluginContext } from 'pinia'

interface PersistenceOptions {
key?: string
storage?: Storage
}

function piniaPersistencePlugin(options: PersistenceOptions = {}) {
return (context: PiniaPluginContext) => {
    const { store } = context
    const key = options.key || `pinia-${store.$id}`
    const storage = options.storage || localStorage
   
    // 从存储中恢复状态
    const stored = storage.getItem(key)
    if (stored) {
      store.$patch(JSON.parse(stored))
    }
   
    // 监听变化并保存
    store.$subscribe((mutation, state) => {
      storage.setItem(key, JSON.stringify(state))
    })
}
}

export default piniaPersistencePlugin类型扩展// types/pinia.d.ts
import 'pinia'

declare module 'pinia' {
export interface PiniaCustomProperties {
    // 可以添加自定义属性
    $customMethod: (value: string) => void
}

export interface DefineStoreOptionsBase<S, Store> {
    // 可以添加 store 配置选项
    persist?: boolean
}
}6. 测试中的类型安全// tests/counter.spec.ts
import { setActivePinia, createPinia } from 'pinia'
import { useCounterStore } from '@/stores/counter'
import { describe, it, expect, beforeEach } from 'vitest'

describe('Counter Store', () => {
beforeEach(() => {
    setActivePinia(createPinia())
})

it('should increment count', () => {
    const store = useCounterStore()
   
    expect(store.count).toBe(0)
    store.increment()
    expect(store.count).toBe(1)
})

it('should calculate double count', () => {
    const store = useCounterStore()
    store.count = 5
   
    expect(store.doubleCount).toBe(10)
})

it('should handle async increment', async () => {
    const store = useCounterStore()
   
    // 模拟 API 调用
    global.fetch = vi.fn().mockResolvedValue({
      json: () => Promise.resolve({ amount: 3 })
    })
   
    await store.incrementAsync()
    expect(store.count).toBe(3)
})
})




页: [1]
查看完整版本: Pinia 与 TypeScript 集成指南