vuex + typescript = vuexok. The bike that rode and overtook everyone

Good day.



Like many developers, I write my own relatively  small project in my spare time  . I used to write in react, but at work I use vue. Well, in order to pump in vue, I began to saw my project on it. At first everything was fine, downright rosy, until I decided that I needed to improve in typescript as well. This is how typescript appeared in my project. And if all the components were  good , then vuex everything was sad. So I had to go through all 5 stages of accepting the problem, well, almost everything.



Negation



Basic requirements for a store:



  1. Typescript types should work in modules
  2. Modules should be easy to use in components, types for states, actions, mutations and getters should work
  3. Do not come up with a new api for vuex, you need to make sure that typescript types somehow work with vuex modules so that you don't have to rewrite the entire application at once
  4. Calling mutations and actions should be as simple and straightforward as possible
  5. The package should be as small as possible
  6. I don't want to store constants with names of mutations and actions
  7. It should work (And what about without it)


It cannot be that such a mature project as vuex did not have normal typescript support. Well, we open  Google  Yandex and drove. I was 100500% sure that everything should be fine with typescript (how wrong I was). There are a lot of different attempts to make friends vuex and typescript. I will give a few examples that I remember, without the code so as not to bloat the article. Everything is in the documentation on the links below.



vuex-smart-module



github.com/ktsn/vuex-smart-module

Good, very good. Everything with me, but personally I did not like the fact that for actions, mutations, states, getters, you need to create separate classes. This, of course, is taste, but this is me and my project) And in general, the issue of typing has not been fully resolved ( comment thread with an explanation why ).



Vuex Typescript Support



Nice attempt, but a lot of rewriting, and generally not accepted by the community.



vuex-module-decorators



This seemed like the perfect way to make vuex and typescript friends. It looks like the vue-property-decorator that I use in development, you can work with the module as with a class, in general, super, but ...



But there is no inheritance. Module classes are not correctly inherited and the issue has been hanging on this problem for a very long time! And without inheritance, there will be a lot of code duplication. Pancake…



Anger



Then it was not at all very much, well, or ± the same - there is no ideal solution. This is the very moment when you say to yourself: Why did I start writing a project in vue? Well, you know react, well, I would write on react, there would be no such problems! At the main work, the project is in vue and you need to upgrade in it - hit the argument. Is it worth the spent nerves and sleepless nights? Sit like everyone else, write komponentiki, no, you need most of all! Throw this vue! Write to react, upgrade in it, and pay more for it!



At that moment, I was ready to hate vue like no other, but it was emotion, and intelligence was still above that. Vue has (in my subjective opinion) many advantages over react, but there is no perfection, as well as winners on the battlefield. Both vue and react are good in their own way, and since a significant part of the project is already written in vue, it would be as foolish as possible to switch to react now. I had to decide what to do with vuex.



Bargain



Well, things are not going well. Maybe then vuex-smart-module? This package seems to be good, yes, you have to create a lot of classes, but it works great. Or maybe he could try writing types for mutations and actions by hand in components and use pure vuex? There, vue3 with vuex4 is on the way, maybe they are doing better with typescript. So let's try pure vuex. In general, this does not affect the work of the project, it still works, there are no types, but you hold on. And hold on)



At first I started doing this, but the code turns out to be monstrous ...



Depression



I had to move on. But where is unknown. It was a completely desperate step. I decided to make a  state container from scratch . The code was drafted in a couple of hours. And it turned out even well. Types work, state is reactive, and even inheritance is there. But soon the agony of despair began to recede, and common sense began to return. All in all, this idea went to the dustbin. By and large, this was the global event bus pattern. And it is only good for small applications. And in general, writing your own vuex is still quite overkill (at least in my situation). Then I already guessed that I was completely exhausted. But it was too late to retreat.



But if anyone is interested, then the code is here: (Probably in vain added this fragment, but the path will be)



not to look nervous
const getModule = <T>(name:string, module:T) => {
  const $$state = {}
  const computed: Record<string, () => any> = {}

  Object.keys(module).forEach(key => {
    const descriptor = Object.getOwnPropertyDescriptor(
      module,
      key,
    );

    if (!descriptor) {
      return
    }

    if (descriptor.get) {
      const get = descriptor.get

      computed[key] = () => {
        return get.call(module)
      }
    } else if (typeof descriptor.value === 'function') {
      // @ts-ignore
      module[key] = module[key].bind(module)
    } else {
      // @ts-ignore
      $$state[key] = module[key]
    }
  })


  const _vm = new Vue({
    data: {
      $$state,
    },
    computed
  })

  Object.keys(computed).forEach((computedName) => {
    var propDescription = Object.getOwnPropertyDescriptor(_vm, computedName);
    if (!propDescription) {
      throw new Error()
    }

    propDescription.enumerable = true
    Object.defineProperty(module, computedName, {
      get() { return _vm[computedName as keyof typeof _vm]},
      // @ts-ignore
      set(val) { _vm[computedName] = val}
    })
  })

  Object.keys($$state).forEach(name => {
    var propDescription = Object.getOwnPropertyDescriptor($$state,name);
    if (!propDescription) {
      throw new Error()
    }
    Object.defineProperty(module, name, propDescription)
  })

  return module
}

function createModule<
  S extends {[key:string]: any},
  M,
  P extends Chain<M, S>
>(state:S, name:string, payload:P) {
  Object.getOwnPropertyNames(payload).forEach(function(prop) {
    const descriptor = Object.getOwnPropertyDescriptor(payload, prop)

    if (!descriptor) {
      throw new Error()
    }

    Object.defineProperty(
      state,
      prop,
      descriptor,
    );
  });

  const module = state as S & P

  return {
    module,
    getModule() {
      return getModule(name, module)
    },
    extends<E>(payload:Chain<E, typeof module>) {
      return createModule(module, name, payload)
    }
  }
}

export default function SimpleStore<T>(name:string, payload:T) {
  return createModule({}, name, payload)
}

type NonUndefined<A> = A extends undefined ? never : A;

type Chain<T extends {[key:string]: any}, THIS extends {[key:string]: any}> = {
  [K in keyof T]: (
    NonUndefined<T[K]> extends Function 
      ? (this:THIS & T, ...p:Parameters<T[K]>) => ReturnType<T[K]>
      : T[K]
  )
}




Adoption The  birth of the bike that has overtaken everyone. vuexok



For the impatient, the code is here , the short documentation is here .



In the end, I wrote a tiny library that covers all the Wishlist and even a little more than was required of it. But first things first.



The simplest vuexok module looks like this:



import { createModule } from 'vuexok'
import store from '@/store'

export const counterModule = createModule(store, 'counterModule', {
  state: {
    count: 0,
  },
  actions: {
    async increment() {
      counterModule.mutations.plus(1)
    },
  },
  mutations: {
    plus(state, payload:number) {
      state.count += payload
    },
    setNumber(state, payload:number) {
      state.count = payload
    },
  },
  getters: {
    x2(state) {
      return state.count * 2
    },
  },
})


Well kind of like vuex, though ... what's on line 10?



counterModule.mutations.plus(1)


Whoa! Is it legal? Well, with vuexok - yes, legally) The createModule method returns an object that exactly repeats the structure of the object of the vuex module, only without the namespaced property, and we can use it to call mutations and actions or to get state and getters, all types are preserved. And from any place where it can be imported.



What about the components?



And with them everything is fine, since in fact it is vuex, then, in principle, nothing has changed, commit, dispatch, mapState, etc. work as before.



But now you can make the types from the module work in the components:



import Vue from 'vue'
import { counterModule } from '@/store/modules/counterModule'
import Component from 'vue-class-component'

@Component({
  template: '<div>{{ count }}</div>'
})
export default class MyComponent extends Vue {
  private get count() {
    return counterModule.state.count // type number
  }
}


The state property in a module is reactive just like in store.state, so to use the module state in Vue components, you just need to return a portion of the module state in a computed property. There is only one caveat. I deliberately made the Readonly state a type, it's not good to change the vuex state so.



Calling actions and mutations is simple to disgrace and the types of input parameters are also saved



 private async doSomething() {
   counterModule.mutations.setNumber(10)
   //   this.$store.commit('counterModule/setNumber', 10)
   await counterModule.actions.increment()
   //   await this.$store.dispatch('counterModule/increment')
 }


Here is such a beauty. A little later, I also needed to react to the change in jwt, which is also stored in the store. And then the watch method appeared in modules. Module watchers work the same way as store.watch. The only difference is that the state and getters of the module are passed as parameters of the getter function.



const unwatch = jwtModule.watch(
  (state) => state.jwt,
  (jwt) => console.log(`New token: ${jwt}`),
  { immediate: true },
)


So what we have:



  1. typed side - yes
  2. types work in components - yes
  3. api like in vuex and everything that was before on pure vuex does not break - is
  4. declarative work with the side - yes
  5. small packet size (~ 400 bytes gzip) - yes
  6. no need to store the names of actions and mutations in constants - there is
  7. it should work - is


In general, it's strange that such a wonderful feature is not available in vuex out of the box, it's awesome how convenient it is!

As for the support for vuex4 and vue3 - I haven't tested it, but judging by the docs it should be compatible.



The problems presented in these articles are also solved:



Vuex - solving an old dispute with new methods

Vuex breaks encapsulation



Wet dreams:



It would be great to make it so that mutations and other actions are available in the context of actions.



How to do this in the context of typescript types - the dick knows it. But if you could do this:



{
  actions: {
    one(injectee) {
       injectee.actions.two()
    },
    two() {
      console.log('tada!')
    }
}


That my joy would have no limit. But life, like typescript, is a harsh thing.



Here's the adventure with vuex and typescript. Well, I sort of spoke out. Thanks for attention.



All Articles