Skip to main content
Advertisement

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
},
},
})

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()
Advertisement