Pinia is written in TypeScript and infers store types automatically. This episode covers basic typing and generics in defineStore, adding types for plugin properties via PiniaCustomProperties, and return type inference for Setup stores and fully typed access to other stores.

One of the reasons big teams choose Pinia is full type safety without writing types repeatedly. Write your store, and TypeScript infers the types of state, getters, actions, even this inside actions — all automatically.
Episode 16 covers TypeScript in Pinia: basic typing and generics in defineStore, adding types for plugin properties with PiniaCustomProperties, and type inference for Setup stores and access to other stores.
Most stores don't need explicit types at all:
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
getters: {
double: (state) => state.count * 2,
},
actions: {
increment() {
this.count++
},
},
})
// Usage
store.count // number
store.double // number
store.increment() // void, this is typedthis.count++ inside the action is directly typed as number without any annotation. TypeScript carries the entire store definition to the places where it's used.
Without inference, every field rename forces you to update types in many places. With inference, the compiler follows the store definition — a single source of truth. This reduces the class of bugs like "state changed but the UI doesn't know the right type".
When state uses a complex structure, define an interface and use it in state:
interface User {
id: number
name: string
}
export const useUserStore = defineStore('user', {
state: (): { current: User | null; list: User[] } => ({
current: null,
list: [],
}),
getters: {
activeUsers: (state) => state.list.filter((u) => u.id > 0),
},
})state: (): { current: User | null; list: User[] } => ... gives Pinia a complete type, so the activeUsers getter and every access to store.list are typed as User.
The same interface can be used in action arguments, so there's no duplication:
actions: {
addUser(user: User) {
this.list.push(user)
},
}addUser(user: User) uses the same User interface as state. If the data structure changes, TypeScript will point to every place that needs updating.
Properties added by plugins aren't known to TypeScript automatically. Extend the Pinia module through module augmentation:
declare module 'pinia' {
export interface PiniaCustomProperties<Id, S, G, A> {
$logger: (message: string) => void
}
}With the declaration above, every store will have a typed $logger:
pinia.use(({ store }) => {
store.$logger = (message) => console.log(`[${store.$id}]`, message)
})PiniaCustomProperties<Id, S, G, A> extends the store instance type so additional plugin properties are recognized — the IDE gives autocomplete and errors when used incorrectly.
A Setup store infers types from the returned values — no extra annotations needed:
export const useCartStore = defineStore('cart', () => {
const items = ref<{ name: string; price: number }[]>([])
function addItem(item: { name: string; price: number }) {
items.value.push(item)
}
return { items, addItem }
})
store.addItem({ name: 'Coffee', price: 15000 })
store.items // Ref<{ name: string; price: number }[]>defineStore('cart', () => ...) infers that items is a Ref and addItem is a function.
Accessing another store from a store is also typed automatically:
const userStore = useUserStore()
userStore.name // string, known to the compiler
userStore.setName('Arman') // argument type is validateduseUserStore() returns a store instance that already has complete types. Passing an argument with the wrong type is rejected by the compiler before it ever reaches runtime.
Tip
For the this type in actions extended by a plugin, use PiniaCustomStateProperties — analogous to PiniaCustomProperties but for state and this.
Episode 16 shows how deeply Pinia integrates with TypeScript. You can now enjoy automatic inference, use interfaces for complex state, type plugin properties with PiniaCustomProperties, and understand return type inference for Setup stores.
Key takeaways:
this in actions.PiniaCustomProperties types additional properties from plugins.In the next episode, episode 17, we'll discuss testing with @pinia/testing — createTestingPinia for a mock pinia, resetting state between tests, mocking actions with vi.fn, and component testing with Vue Test Utils and Vitest.