typescriptvue.jspinia

Use a generic with Pinia useStore to type a store property?


I have an auth store that holds the currently logged in user's profile. There are different types of profiles though, and the definition is a union type. But I also have components that can only be accessed by users with a StaffProfile. I would like to be able to do something like pass in the type to useAuthStore to have that property specifically typed in that usage.

Here's the general setup of the store:

interface State {
    profile: StaffProfile | ClientProfile | null
}

export const useAuthStore = defineStore('auth', {
    state: () : State => ({
        profile: null,
    }),
    // ...
});

and currently I can do this and it works

const authStore = useAuthStore(); 
const profile = authStore.profile as StaffProfile;

// authStore.profile is StaffProfile | ClientProfile | null
// profile is StaffProfile

But my ideal solution would be to wind it up something like this:

const authStore = useAuthStore<StaffProfile>();
// authStore.profile is StaffProfile

Solution

  • Pinia stores and store composables don't work this way. If these were 2 different stores, a generic function like createStaffOrClientStore that wraps defineStore could be used. Doing this for a single store would require to redefine useAuthStore type that relies on inferred types, which isn't practical.

    The assertion store.profile as StaffProfile is generally an acceptable way to do this. Notice that type assertion is always a risk for type safety, in this case it also ignores the possibility of null value.

    A getter function can be created specifically for this objective, which is a common approach for typing purposes in TypeScript:

    getters: {
      getProfile() {
        return <T extends StaffProfile | ClientProfile>() => (this.profile as T)
      }
    }
    

    If this is not the only value in a store that depends on the intended type, this is the case for a composable that abstracts from the store and contain a subset of it for the specific usage:

    const useProfile = <T extends StaffProfile | ClientProfile>() => {
      const store = useAuthStore();
      const { profile } = storeToRefs(store);
      const { updateProfile } = store(); 
    
      return {
        profile: profile as Ref<T>,
        updateProfile: updateProfile<T>
      }
    }
    
    const useStaffProfile = useProfile<StaffProfile>;