How to correctly type vuex modules in vue 3 and typescript
Asked Answered
C

1

7

I am trying to figure out how to type vuex modules in a vue 3 typescript project. The official documentation is lacking in this area.

Assume I have a project like this:

import { createStore, useStore as baseUseStore, Store } from 'vuex';
import { InjectionKey } from 'vue';

interface FruitState  {
    apple: boolean,
    peach: boolean,
    plum: boolean
}

const FruitModule = {
    namespaced: true,
    state: (): FruitState => ({
      apple: true,
      peach: false,
      plum: true
    }),
    mutations: {},
    action: {}
}


export interface State {
    foo: string;
  }
  
  export const key: InjectionKey<Store<State>> = Symbol();
  
  export const store = createStore<State>({
      modules: {
        fruit: fruitModule
      },
      state: {foo: 'foo'},
      mutations: { 
        changeFoo(state: State, payload: string){
            state.foo = payload
        }
      },
      actions: { 
        setFooToBar({commit}){
         commit('changeFoo', 'bar')
      }}
  })

  export function useStoreTyped() {
    return baseUseStore(key);
  }
  

... then later in a component:

  const apple = computed(() => store.state.fruit.apple);

When I try to access apple it does not work because it throws error Property 'fruit' does not exist on type 'State'

Now IF I do something like this:

import { createStore, useStore as baseUseStore, Store } from 'vuex';
import { InjectionKey } from 'vue';

interface FruitState  {
    apple: boolean,
    peach: boolean,
    plum: boolean
}

const FruitModule = {
    namespaced: true,
    state: (): FruitState => ({
      apple: true,
      peach: false,
      plum: true,
    }),
    mutations: {},
    action: {}
}


export interface State {
    foo: string;
    fruit?: FruitState;
  }
  
  export const key: InjectionKey<Store<State>> = Symbol();
  
  export const store = createStore<State>({
      modules: {
        fruit: fruitModule
      },
      state: {foo: 'foo'},
      mutations: { 
        changeFoo(state: State, payload: string){
            state.foo = payload
        }
      },
      actions: { 
        setFooToBar({commit}){
         commit('changeFoo', 'bar')
      }}
  })

  export function useStoreTyped() {
    return baseUseStore(key);
  }

And try again, the error changes to Object is possibly 'undefined'

It will allow me let me access the fruit module if I use the optional chaining ?.

As in const apple = computed(() => store.state.fruit?.apple);

But this doesn't seem acceptable to me since I know that fruit.apple is actually not undefined.

What's the correct way to include a module in your state types for vuex?

Chanda answered 19/2, 2022 at 17:30 Comment(5)
fruit is declared as optional in the State interface, so it's therefore possibly undefined, and it's access must always be checked. Making it optional implies that store.state.fruit might be (re)-assigned to undefined or null (e.g., in between calls). If you know fruit will always be defined, then don't make it optional in State. Why have you made it optional to begin with?Wilke
Vuex automatically places the fruit prop in the state object that is constructed from the createStore function, this is what the namespaced: true option is for on the module. Making a fruit property on State interface was my attempt to let TS know that the module will eventually exist. And making it optional was so that I didn't have to include it in the initial top level state, because it is supposed to be set in the module.Chanda
It will exist immediately and not eventually, unless you use dynamic modules.Apoenzyme
Sorry, maybe eventually wasn't the right term. But I mean it will exist once the createStore function is invoked.Chanda
IIRC I declared two separate types, for root state and the whole state with modules. useState should be used as a generic, and you may want to use the whole state in useState<State>().Apoenzyme
W
3

You don't need to make the fruit state optional in the State interface:

export interface State {
  foo: string;
  //fruit?: FruitState; 
  fruit: FruitState;
}

Based on your comment, you were trying to get around this TypeScript error when declaring the root state (as seen here):

export const store = createStore<State>({
  modules: {
    fruit: fruitModule
  },
  state: { foo: 'foo' }, // ❌ Property 'fruit' is missing in type '{ foo: string; }' but required in type 'State'.
})

Use type assertion as a workaround:

export const store = createStore<State>({
  modules: {
    fruit: fruitModule
  },
  state: { foo: 'foo' } as State, // ✅
})

demo

Wilke answered 19/2, 2022 at 18:27 Comment(1)
Thank you, this works, but I'm wondering what a long term solution would be. Do you know whether the Vuex team is aware of this/working on it?Estis

© 2022 - 2024 — McMap. All rights reserved.