How to Type Pinia Store with Typescript

In this tutorial, I will demonstrate the art of using Typescript to type a Pinia store. with these powerful technologies combined, We can supercharge our development experience and write safer and more maintainable global stores.

You can find the examples on GitHub. Let’s get started with the State!

How to Type Pinia State

As an example, we will build and type step by-step a store that will hold a list of customers. We will also add getters and actions.

First, let’s start with the setup. Usually, you would place the store under src/stores folder and create it using defineStore function from Pinia:

import { defineStore } from 'pinia'

export const useCustomers = defineStore('customers', {
  state: () => ({
    customers: [],
    isRequestLoading: false,
  }),
})

Then in your component, you would consume this store using useCustomers and display the data as needed:

<script setup lang="ts">
  import { useCustomers } from './store/customers/customers'
  import { storeToRefs } from 'pinia'

  const customersStore = useCustomers()
  const { customers, isRequestLoading } = storeToRefs(customersStore)
</script>

<template>
  <p v-if="isRequestLoading">Loading...</p>

  <p v-else>number of customers {{ customers.length }}</p>
</template>

As you can notice, without specifying any Typescript types, we get auto-completion and correct types already.

Pinia state Typescript basic types

This works fine with basic types like boolean, number, or string. However, if you use the customers array, there are no types, since the value is just an empty array initially.

Pinia state Typescript missing autocompletion.

Doing customers[0]. would not suggest any auto-completion. this is where adding Typescript types would be beneficial. Let’s go ahead and do just that:

import { defineStore } from 'pinia'

type Customer = {
  fullName: string
  isActive: boolean
  totalSpending: number
}

type State = {
  customers: Customer[]
  isRequestLoading: boolean
}

export const useCustomers = defineStore('customers', {
  state: (): State => ({
    customers: [],
    isRequestLoading: false,
  }),
})

That was the first approach, where we specify a State type and then use it as the return value for the state function. You can also use an interface instead of type. Comparatively, we can do it like this also:

import { defineStore } from 'pinia'

type Customer = {
  fullName: string
  isActive: boolean
  totalSpending: number
}

export const useCustomers = defineStore('customers', {
  state: () => ({
    customers: <Customer[]>[],
    isRequestLoading: false,
  }),
})

Despite the second approach producing less code, I would opt to use the first one to avoid Type Assertion.

Now we will have autocompletion and the correct types when using the state:

Pinia state Typescript shows autocompletion.
yaaay! 🤩

Let’s how to type getters next!

How to Type Pinia Getters

Since getters are functions, we can type them in the same manner as the state. as an example let’s add a getter that computes the total number of active customers. first, create the getter:

import { defineStore } from 'pinia'

type Customer = {
  id: string
  fullName: string
  isActive: boolean
  totalSpending: number
}

type State = {
  customers: Customer[]
  isRequestLoading: boolean
}

export const useCustomers = defineStore('customers', {
  state: (): State => ({
    customers: [],
    isRequestLoading: false,
  }),
  getters: {
    activeCustomersCount({ customers }) {
      return customers.filter(({ isActive }) => isActive).length
    },
  },
})

This is already sufficient since Typescript will infer the type from the return statement. However, I wouldn’t want to rely on inference when it comes to functions. It would be better to provide the type in the declaration, to make it more obvious what the return type should be when reading the code. To do so, add the return type to the function signature:

import { defineStore } from 'pinia'

type Customer = {
  id: string
  fullName: string
  isActive: boolean
  totalSpending: number
}

type State = {
  customers: Customer[]
  isRequestLoading: boolean
}

export const useCustomers = defineStore('customers', {
  state: (): State => ({
    customers: [],
    isRequestLoading: false,
  }),
  getters: {
    activeCustomersCount({ customers }): number {
      return customers.filter(({ isActive }) => isActive).length
    },
  },
})

Furthermore, it is beneficial, to bind this function to a simple contract, which is to return a number. In case of any change to the logic inside, Typescript will complain if we unintentionally make it return something different than what is expected.

Moving on, there is another type of getter. It returns a function that accepts arguments and uses them to derive the final return value:

import { defineStore } from 'pinia'

type Customer = {
  id: string
  fullName: string
  isActive: boolean
  totalSpending: number
}

type State = {
  customers: Customer[]
  isRequestLoading: boolean
}

export const useCustomers = defineStore('customers', {
  state: (): State => ({
    customers: [],
    isRequestLoading: false,
  }),
  getters: {
    activeCustomersCount({ customers }): number {
      return customers.filter(({ isActive }) => isActive).length
    },
    getCustomerById({ customers }): (id: string) => Customer | undefined {
      return (id: string): Customer | undefined => {
        return customers.find((customer) => customer.id === id)
      }
    },
  },
})

We added getCustomerById which accepts an id and then returns the customer with an id matching it. Furthermore, we added an id field to the Customer type.

Pinia typescript getters with arugments

Next, let’s check out actions.

How to Type Pinia Actions

When it comes to actions, they are usually used for side effects. mostly to perform async tasks. Let’s create an action called fetchCustomers:

import { defineStore } from 'pinia'

type Customer = {
  id: string
  fullName: string
  isActive: boolean
  totalSpending: number
}

type State = {
  customers: Customer[]
  isRequestLoading: boolean
}

export const useCustomers = defineStore('customers', {
  state: (): State => ({...}),
  getters: {...},
  actions: {
    async fetchCustomers(): Promise<void> {
      const promise: Promise<Customer[]> = new Promise((resolve) => {
        resolve([
          {
            id: '1',
            fullName: 'John Doe',
            isActive: false,
            totalSpending: 3000,
          },
          {
            id: '2',
            fullName: 'Jane Doe',
            isActive: true,
            totalSpending: 5000,
          },
        ])
      })

      this.customers = await promise
    },
  },
})

Since this is an example I am using dummy data, in a real-world scenario, you would probably be using a service instead.

Closing Thoughts

Using Typescript brings all sorts of joy to your development experience. and Pinia is a nice and simple global state management system that plays well with the Vue Composition API!

I would strongly suggest that you start adding types to your store if you are currently using Typescript. If not, now you have another reason to convince your manager that you need it in your enterprise codebase.

Amenallah Hsoumi
Amenallah Hsoumi

Senior Software Engineer, Indie Hacker, building cool stuff on the web!

Articles: 19