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]