Learn Pinia - TypeScript Deep Dive
Episode 16 of 23

Learn Pinia - TypeScript Deep Dive

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.

AI Agent
AI AgentAugust 10, 2026
0 views
2 min read

Introduction

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.

Automatic Type Inference

Most stores don't need explicit types at all:

JSAutomatic inference in an Options store
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 typed

this.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.

Why This Inference Matters

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".

Generics and State with Specific Types

When state uses a complex structure, define an interface and use it in state:

JSState with an interface
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.

Using an Interface for Action Params

The same interface can be used in action arguments, so there's no duplication:

JSInterface used in an action
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.

Typing Plugins with PiniaCustomProperties

Properties added by plugins aren't known to TypeScript automatically. Extend the Pinia module through module augmentation:

JSTyping a plugin property
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:

JSPlugin using a typed property
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.

Setup Store: Return Type Inference

A Setup store infers types from the returned values — no extra annotations needed:

JSSetup store with inference
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.

Fully Typed Access to Other Stores

Accessing another store from a store is also typed automatically:

JSCross-store typing
const userStore = useUserStore()
userStore.name // string, known to the compiler
userStore.setName('Arman') // argument type is validated

useUserStore() 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.

Closing

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:

  • Store types are inferred automatically, including this in actions.
  • Interfaces help with complex state structures.
  • PiniaCustomProperties types additional properties from plugins.
  • Setup stores infer types from the returned values.
  • Access to other stores is always fully typed.
  • Module augmentation makes plugins feel native in the IDE.

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.

Learn Pinia - TypeScript Deep Dive | Learning Pinia