12.4 Pinia + TypeScript — Store Type Inference and Actions
Pinia Setup
npm install pinia
// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')
defineStore Types — Option Store
// stores/counter.ts
import { defineStore } from 'pinia'
// Option Store — similar to Vuex
export const useCounterStore = defineStore('counter', {
// state: types automatically inferred
state: () => ({
count: 0,
name: 'counter',
}),
// getters: access state via `state`
getters: {
doubleCount: (state) => state.count * 2,
// Access other getters
doubleCountPlusOne(): number {
return this.doubleCount + 1
},
},
// actions: async supported
actions: {
increment() {
this.count++
},
decrement() {
this.count--
},
async fetchAndSet(id: number) {
const data = await fetch(`/api/counter/${id}`).then(r => r.json())
this.count = data.value
},
},
})
defineStore Types — Setup Store (Recommended)
Composition API style — more flexible and TypeScript-friendly.
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
interface User {
id: string
name: string
email: string
role: 'admin' | 'user'
}
export const useUserStore = defineStore('user', () => {
// state — use ref
const currentUser = ref<User | null>(null)
const isLoading = ref(false)
const error = ref<string | null>(null)
// getters — use computed
const isLoggedIn = computed(() => currentUser.value !== null)
const isAdmin = computed(() => currentUser.value?.role === 'admin')
const displayName = computed(
() => currentUser.value?.name ?? 'Guest'
)
// actions — regular functions
async function login(email: string, password: string) {
isLoading.value = true
error.value = null
try {
const user = await authService.login(email, password)
currentUser.value = user
} catch (e) {
error.value = e instanceof Error ? e.message : 'Login failed'
throw e
} finally {
isLoading.value = false
}
}
function logout() {
currentUser.value = null
authService.logout()
}
function updateProfile(updates: Partial<Pick<User, 'name' | 'email'>>) {
if (!currentUser.value) return
currentUser.value = { ...currentUser.value, ...updates }
}
return {
// state
currentUser,
isLoading,
error,
// getters
isLoggedIn,
isAdmin,
displayName,
// actions
login,
logout,
updateProfile,
}
})
Store Usage Patterns
Using Stores in Components
<script setup lang="ts">
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'
const userStore = useUserStore()
// storeToRefs: destructure while preserving reactivity
const { currentUser, isLoggedIn, displayName, isLoading } = storeToRefs(userStore)
// Actions are not reactive, so destructure directly
const { login, logout, updateProfile } = userStore
async function handleLogin(email: string, password: string) {
await login(email, password)
}
</script>
<template>
<div>
<p v-if="isLoading">Logging in...</p>
<div v-else-if="isLoggedIn">
<p>Welcome, {{ displayName }}!</p>
<button @click="logout">Logout</button>
</div>
<LoginForm v-else @submit="handleLogin" />
</div>
</template>
Extracting Store Types
// Extract store types
import { useUserStore } from '@/stores/user'
type UserStore = ReturnType<typeof useUserStore>
type UserState = ReturnType<UserStore['$state']>
// Extract specific types
type LoginAction = UserStore['login']
Cross-Store Communication
// stores/cart.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { useUserStore } from './user'
export const useCartStore = defineStore('cart', () => {
const items = ref<CartItem[]>([])
const userStore = useUserStore() // Use another store
const total = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
async function checkout() {
if (!userStore.isLoggedIn) {
throw new Error('Login required.')
}
await orderService.create({
userId: userStore.currentUser!.id,
items: items.value,
})
items.value = [] // Clear cart
}
return { items, total, checkout }
})
Pinia Plugin Types
// plugins/persistPlugin.ts
import type { PiniaPluginContext } from 'pinia'
function persistPlugin({ store }: PiniaPluginContext) {
// Restore from localStorage
const stored = localStorage.getItem(store.$id)
if (stored) {
store.$patch(JSON.parse(stored))
}
// Watch for changes and save
store.$subscribe((_, state) => {
localStorage.setItem(store.$id, JSON.stringify(state))
})
}
// Register in main.ts
const pinia = createPinia()
pinia.use(persistPlugin)
Pro Tips
1. Batch updates with $patch
// Update multiple states at once (recorded as one history entry)
store.$patch({
count: 10,
name: 'New Name',
})
// Function form (for complex updates)
store.$patch((state) => {
state.items.push(newItem)
state.total += newItem.price
})
2. Reset state with $reset (Option Store only)
// Option Store
const store = useCounterStore()
store.$reset() // Resets state to initial values
3. Detect state changes with $subscribe
const unsubscribe = store.$subscribe((mutation, state) => {
console.log('Store changed:', mutation.type, state)
})
// Unsubscribe
unsubscribe()