Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Write Stores in Typescript #564

Open
Anonyfox opened this issue Jan 4, 2017 · 64 comments · May be fixed by #1121
Open

Write Stores in Typescript #564

Anonyfox opened this issue Jan 4, 2017 · 64 comments · May be fixed by #1121

Comments

@Anonyfox
Copy link

@Anonyfox Anonyfox commented Jan 4, 2017

I couldn't find anything on google, so is this conveniently possible? Basically the bummer for me is that I have to call dispatch/commit with a given identifier string to trigger something.

My vision is to define stores as Typescript classes with typed methods (getting intellisense in editor and so on) which I can call instead of dispatch("my_action", data) and having to look up each and everything and check for errors manually all the time.

Basically my problem is that my team is building a fairly large Vue/Vuex frontend for a core product of our company, consisting of already 18 fairly complex Stores as modules, and this year stuff is going to at least quadruple in size and complexity. We already decided to go on with typescript instead of ES7 and are in the process of migrating our backend/frontend code, but this thing really feels like a bummer.

I think this is not the typical "small" use case for Vue.js, since we're building a huge enterprise frontend with loads of requirements and edge cases, but isn't vuex(/flux) supposed to scale up when complexity rises?

Has anyone experience in building complex, type safe Vuex 2.0 stores in Typescript yet? Any help here would be appreciated

@LinusBorg

This comment has been minimized.

Copy link
Member

@LinusBorg LinusBorg commented Jan 4, 2017

/ping @ktsn

@ktsn

This comment has been minimized.

Copy link
Member

@ktsn ktsn commented Jan 4, 2017

Thank you for trying Vuex with TypeScript.
Honestly, it is challenge to achieve type safety with pure Vuex API. But we currently can specify action/mutation type as type parameter to let TS compiler can infer the correct type.

// Declare each action type
type ActionA = {
  type: 'A',
  value: number
}

type ActionB = {
  type: 'B',
  value: string
}

// Declare the union type of actions
type Action = ActionA | ActionB

// Provide action type
store.dispatch<Action>({
  type: 'A',
  value: 1
})

About defining the store as class, I believe we can make such binding like vue-class-component and I personally interested in it. Also, there is an experiment for type safety of Vuex. https://github.com/HerringtonDarkholme/kilimanjaro

@Anonyfox

This comment has been minimized.

Copy link
Author

@Anonyfox Anonyfox commented Jan 4, 2017

Mh, okay, this might be a start. For better clarification, I'll provide a simplified example for a store that will later get included as a module:

state.ts:

export class State {
    // persons above 24 years old
    adults: number = 2

    // persons between 16 and 24 years old
    juveniles: number = 0

    // persons below 16 years old
    children: number = 0
}

mutations.ts:

import { State } from './state'

export type personIdentifier = 'adults' | 'juveniles' | 'children'

export class Mutations {
    // increment the chosen persons type by one
    inc (state: State, key: personIdentifier) {
        state[key]++
    }

    // decrement the chosen persons type by one
    dec (state: State, key: personIdentifier) {
        state[key]--
    }
}

actions.ts:

import { Store } from 'vuex'
import { State } from './state'

export class Actions {
    inc ({ commit }: Store<State>) {
        // ??????
    }
}

skipping the getters for now, since they're unimportant. This is where I am currently, and I would combine these classes into a Store with namespaced: true, since this store might be used on multiple places independently for several UI components.

Is there a solution how I might write these actions as type safe methods? Your example is not clear enough for me to apply the pattern you provided, I think.

@ktsn

This comment has been minimized.

Copy link
Member

@ktsn ktsn commented Jan 4, 2017

Hm, there maybe no solution when namespaced: true is used since we cannot combine string literal type 😞
I guess we need some wrapper to trick the type checking.

@Anonyfox

This comment has been minimized.

Copy link
Author

@Anonyfox Anonyfox commented Jan 6, 2017

small heads-up: it was really straightforward to implement a Store-Module as a standalone npm package in typescript, with tooling/tests/typings and namespaced:true, and then use it dynamically within the core app.

Thanks to the recent efforts of you guys for the typescript support, implementing the interfaces really helped to get it right immediately!

In my opinion the only puzzle-piece left is typesafe commit/dispatch (with namespaced), but I think this is a hard problem to bolt-on vuex as-is. One idea would be to generate types on the "new Vuex.Store" call in the main code, which map the generated identifier-string to the underlying function or sth like that. But ideally, there could be an alternate way to call commit/dispatch, maybe through wrapper classes to Module that does the actual calling behind the scenes or something. This seems to be an architectural issue, which is really not easy to resolve.

On the other hand I think it would really, really great to solve this. Even if approx. most vuex users do "just" use ES7, they could benefit from intellisense/hints from their Editors/IDEs greatly, providing huge ergonomic value once stores become nontrivial in size.

@morhi

This comment has been minimized.

Copy link

@morhi morhi commented Jan 17, 2017

@Anonyfox Would you mind creating an example repository or snippet for vuex with typescript, modules and classes as far as you got it so far? I am currently setting up a vue project which probably will contain a few store modules. I understand using vuex with ES6 but I am struggling with properly setting up my project with typescript. I would like to have everything structured well but it seems a bit complicated at the moment :)

As for now I created two global files (mutations.ts and actions.ts) where I export some constants of the mutation types and actions that all stores use. These files are imported both in the store modules and in the components that use the store so I don't need to use string as identifiers.

@Anonyfox

This comment has been minimized.

Copy link
Author

@Anonyfox Anonyfox commented Jan 18, 2017

@morhi sure, I extracted a simple (but not trivial) store we're using.

https://github.com/Anonyfox/vuex-store-module-example

please respect that I can not give a valid license, this is just intended for demonstrating.

/cc @ktsn

@wonderful-panda

This comment has been minimized.

Copy link

@wonderful-panda wonderful-panda commented Jan 19, 2017

I think it would be nice if we can use namespaced module like as

/*
 * equivalent to ctx.commit("foo/bar/action", arg)
 */
ctx.commit.foo.bar("action", arg);
// OR
ctx.foo.bar.commit("action", arg);
// OR
ctx.modules.foo.bar.commit("action", arg);

/*
 * equivalent to ctx.getters("foo/bar/prop")
 */
ctx.getters.foo.bar.prop
// OR
ctx.foo.bar.getters.prop
// OR
ctx.modules.foo.bar.getters.prop

This will make adding and combining types more easy.

@morhi

This comment has been minimized.

Copy link

@morhi morhi commented Jan 19, 2017

@Anonyfox wow! thank you very much! I adopted your example into my environment and got it working 👍

@Glidias

This comment has been minimized.

Copy link

@Glidias Glidias commented Jan 24, 2017

Related: #532

One way to ensure type consnistency between dispatch/commit payload vs. action/mutation handler, is to declare both of them at once. Eg.

http://tinyurl.com/go9ap5u

However, you'd need some sort of runtime decoration approach to setup necessary pre-initializations prior to app running. Also, Typescript doesn't seem to complain if I override a method with a different payload type parameter form, which is something that i'd need to enforce, though (anyway to do this?). Overall, setting up all the decorators and custom initilizations at runtime is a lot of work.

@wonderful-panda

This comment has been minimized.

Copy link

@wonderful-panda wonderful-panda commented Jan 24, 2017

FYI, I wrote helper to make store half-decent type safe.
https://gist.github.com/wonderful-panda/46c072497f8731a2bde28da40e9ea2d7

It seems to work well except namespaced module.

@Glidias

This comment has been minimized.

Copy link

@Glidias Glidias commented Jan 26, 2017

@wonderful-panda Is it possible with your code to add modules within modules recursively?

@wonderful-panda

This comment has been minimized.

Copy link

@wonderful-panda wonderful-panda commented Jan 26, 2017

@Glidias yes.

Nested module example is below:

const module1 = builder.createModule<...>({ /* module1 definition */ });
const module2 = builder.createModule<...>({ /* module2 definition */ });

const parentModule = 
    builder.addModule("bar", module1)
           .addModule("baz", module2)    // { modules: { bar: module1, baz: module2 } }
           .createModule<...>({ /* parentModule definition without `modules` */ });

const store =
    builder.addModule("foo", parentModule)
           .createStore<...>({ /* store options without `modules` */ });
@Glidias

This comment has been minimized.

Copy link

@Glidias Glidias commented Jan 27, 2017

@wonderful-panda See the comments in the gist for continued discussion in that area.

Regarding dynamic namespacing of modules + strict typing, one idea i was considering is that after a module tree is initialized, you traverse through it's state tree and will always set a _ property under each module state within the tree (including root store state for the sake of homogeneity), in order to have their namespace path references stored dynamically at runtime. That way, you can simply use rootState.moduleA.moduleB._, to retrieve a path accordingly. Or if the context is unknown within actions (may/may not be root), you can use context.state._. However, this will require you to strictly hardcode the module interface field references (under each State for typehinting) if you're not using the builder utility. Also, a _ property reference must be set up as well per module state to ensure you get typehinting/type-completion. The Builder already sets up combined state for module references under a given parent state, so your Module state would probably just implement a dummy IPrefixedState interface that provides an optional readonly _?:string parameter that will give you boilerplate typehinting.

For strictly typed dispatches of module-specific mutations/actions, one way is to adopt a generic helper wrapper method to commit something through a given context (with/without namespace prefixing..)

https://github.com/Glidias/vuex-store-module-example/blob/master/src/util/vuexhelpers.ts

However, this will result in a bit of additional performance overhead of calling the wrapper helper function (remember, there's no inlining in Typescript). But i guess, it's okay and shouldn't be too much of an issue.

 import { MutationTypes } from './mutations';

import * as VuexHelper from "./util/vuexhelpers"
const commitTo = VuexHelper.getCommitToGeneric<MutationTypes>();

// within action handler context (affecting another module somewhere else..)
commitTo(context,"INC", "adults", {root:true}, context.rootState.someModule.anotherNestedModule._)

  // if a particular set of action handlers aren't aware if it's registered under a namespaced:true module or not
  commitTo(context,"INC", "adults", {root:true}, context.state._)

 // or something like this within a Component context:
commitTo(this.$store,"INC", "adults", undefined, $store.state.someModule.anotherNestedModule._)

Also, if your module needs to respond to both namespaced vs non-namespaced mutations/actions, namespaced:true vuex setting won't work well anyway. So, here's a possible approach in userland: https://github.com/Glidias/vuex-store-module-example/wiki/Managing-mixed-namespacings-between-module's-mutations-and-actions

@snaptopixel

This comment has been minimized.

Copy link
Contributor

@snaptopixel snaptopixel commented Feb 16, 2017

I've done some work towards this end and the solution is coming along nicely. I've created a set of decorators similar to vue-class-component, which combine with a few conventions to make a pretty nice experience in TS. It's currently light on documentation (like, none heh) but the tests tell the story pretty well, I hope... https://github.com/snaptopixel/vuex-ts-decorators/blob/master/test/index.ts

I've invited @ktsn and @yyx990803 as collaborators but happy to receive ideas and pull requests from anyone. I'll be writing some docs and examples asap.

@ktsn

This comment has been minimized.

Copy link
Member

@ktsn ktsn commented Feb 16, 2017

Thank you very much for all of your works!
I personally investigating this topic recently and created an experimental package of Vuex like store library. It actually does not Vuex but I believe the mechanism can help us to achieve Vuex's type safety somehow 🙂

@snaptopixel

This comment has been minimized.

Copy link
Contributor

@snaptopixel snaptopixel commented Feb 16, 2017

Nice @ktsn I'll take a look at your work, thanks for the heads up! Give mine a look too as you find time.

@chanon

This comment has been minimized.

Copy link

@chanon chanon commented Feb 18, 2017

@snaptopixel I just tried out your decorators and I think they are pretty great!

Very simple to write and use. Defining stores isn't too different from normal Vuex and using them is exactly the same as normal Vuex except the added type safety (including with commit and dispatch!) which is nice!

However there are a few things that I could not get to work, so I added some questions/issues to your repository.

@snaptopixel

This comment has been minimized.

Copy link
Contributor

@snaptopixel snaptopixel commented Feb 19, 2017

Awesome @chanon thank you so much for trying them out and filing issues. That helps tremendously. I'm planning on adding a proper readme soon and will definitely be working on the issues posted.

@istrib

This comment has been minimized.

Copy link

@istrib istrib commented May 6, 2017

My team was looking for a simple, tiny and unobtrusive solution. After several iterations I distilled it to this: https://github.com/istrib/vuex-typescript.
Using wrapper functions as a proxy to store.dispatch/commit/getters seemed most natural and higher-order functions with TypeScript type inference removed most boilerplate.
I specifically DID NOT want to use classes (there is nothing to encapsulate). Using ES6 modules to group actions/getters/mutations of the same Vuex module provides enough structure IMO.

//Vuex Module (like in JS + type annotations, no classes):

export const basket = {
    namespaced: true,

    mutations: {
        appendItem(state: BasketState, item: { product: Product; atTheEnd: boolean }) {
            state.items.push({ product: item.product, isSelected: false });
        },
    ...

//Strongly-typed wrappers:

const { commit } =
     getStoreAccessors<BasketState, RootState>("basket"); // Pass namespace here, if we make the module namespaced: true.

// commit is a higher-order function which gets handler function as argument
// and returns a strongly-typed "accessor" function which internally calls the standard store.commit() method.
// Implementation of commit is trivial: https://github.com/istrib/vuex-typescript/blob/master/src/index.ts#L103

// you get intellisense here:
export const commitAppendItem = commit(basket.mutations.appendItem);

// commitAppendItem is a function with strongly-typed signature
// so you get intellisense for function name and types of its arguments here too:

import * as basket from "./store/basket";
basket.commitAppendItem(this.$store, newItem);
@mrcrowl

This comment has been minimized.

Copy link

@mrcrowl mrcrowl commented Jun 17, 2017

@istrib I really like what you've done in vuex-typescript.

I just wanted to take it a bit further—if you don't agree these ideas, that's totally okay. If you do agree, I'd be keen to incorporate these changes into vuex-typescript somehow.

My main changes are:

  • Avoid passing $store/context to the accessor methods: we can encapsulate these within the accessors by providing the store later:
    i.e. basket.commitAppendItem(newItem) should be sufficient.
  • No need to distinguish between payload / payload-less versions of commit + dispatch.
    Typescript overloads solve this problem.
  • Promises returned from dispatch should be strongly-typed.
  • Assumes namespaced modules

I also took the point of view that we don't need to start with a vuex-store options object. If we treat the accessor-creator as a builder, then the store can be generated:

import { getStoreBuilder } from "vuex-typex"
import Vuex, { Store, ActionContext } from "vuex"
import Vue from "vue"
const delay = (duration: number) => new Promise((c, e) => setTimeout(c, duration))

Vue.use(Vuex)

export interface RootState { basket: BasketState }
export interface BasketState { items: Item[] }
export interface Item { id: string, name: string }

const storeBuilder = getStoreBuilder<RootState>()
const moduleBuilder = storeBuilder.module<BasketState>("basket", { items: [] })

namespace basket
{
    const appendItemMutation = (state: BasketState, payload: { item: Item }) => state.items.push(payload.item)
    const delayedAppendAction = async (context: ActionContext<BasketState, RootState>) =>
    {
        await delay(1000)
        basket.commitAppendItem({ item: { id: "abc123", name: "ABC Item" } })
    }

    export const commitAppendItem = moduleBuilder.commit(appendItemMutation)
    export const dispatchDelayedAppend = moduleBuilder.dispatch(delayedAppendAction)
}
export default basket

/// in the main app file
const storeBuilder = getStoreBuilder<RootState>()
new Vue({
    el: '#app',
    template: "....",
    store: storeBuilder.vuexStore()
})
@Shepless

This comment has been minimized.

Copy link

@Shepless Shepless commented Jun 17, 2017

What would be the recommended approach here? We are starting a new project at work and are really keen to have type safety on our stores (including commit/dispatch).

@istrib

This comment has been minimized.

Copy link

@istrib istrib commented Jun 20, 2017

I like your approach, @mrcrowl
I am successfully using a similar builder on one of my current projects. Works really well.

With https://github.com/istrib/vuex-typescript I searched for a solution which produces code that is identical to vanilla Vuex + a bit of simple stuff below it. For that reason I did not mind starting with Vuex options rather than have them built. That is also why I decided to explicitly pass $store into accessors. I like your variation for doing that much while still being tiny.

https://github.com/istrib/vuex-typescript now contains your two great suggestions: making promises returned from dispatch strongly-typed and using function overloads for mutations/actions without payload.

@Shepless

This comment has been minimized.

Copy link

@Shepless Shepless commented Jun 20, 2017

@istrib do you have gist example for reference please?

@istrib

This comment has been minimized.

Copy link

@istrib istrib commented Jun 21, 2017

@Shepless There is a complete example here with this file giving the best overview.

@michaelharrisonroth

This comment has been minimized.

Copy link

@michaelharrisonroth michaelharrisonroth commented Aug 7, 2017

Thanks @istrib for the starting point. I am looking to get Vuex setup with typescript and I just stumbled across vuex-typescript. Unfortunately, I am struggling to get it working correctly. Is there a reference of how the store is actually instantiated?

I am doing:

import * as Vue from 'vue';
import * as Vuex from 'vuex';

// vuex store
import {createStore} from './store';

Vue.use(Vuex);

new Vue({
  el: '#app-main',
  store: createStore()
});

But I am getting the error:

[vuex] must call Vue.use(Vuex) before creating a store instance

My directory structure is pretty close to the example:
https://github.com/istrib/vuex-typescript/tree/master/src/tests/withModules/store

@DevoidCoding

This comment has been minimized.

Copy link

@DevoidCoding DevoidCoding commented Aug 7, 2017

I've done an impl. of the two approach in a Todo app. Here's the repo Vue.js + Vuex (typescript & typex) • TodoMVC


@michaelharrisonroth You're certainly doing

export const createStore = new Vuex.Store<State>({

expect of

`export const createStore = () => new Vuex.Store<State>({`

in your store.ts file

@michaelharrisonroth

This comment has been minimized.

Copy link

@michaelharrisonroth michaelharrisonroth commented Aug 7, 2017

@DevoidCoding That was the issue and your TodoMVC is extremely helpful, thank you!

@ktsn ktsn linked a pull request that will close this issue Jan 4, 2018
@ktsn

This comment has been minimized.

Copy link
Member

@ktsn ktsn commented Jan 4, 2018

I just made a PR to improve Vuex typings. #1121
It would be appreciated if you can input any suggestion/comments 🙂

@ktsn ktsn added the typescript label Jan 11, 2018
@danielfigueiredo

This comment has been minimized.

Copy link

@danielfigueiredo danielfigueiredo commented Mar 21, 2018

I tried writing better types for Vuex and I thought about making a PR with these changes, but it doesn't really work completely. The problem is on how modules are created, the rootState, rootGetters, and some other concepts of Vuex that just merges isolated objects and makes typing safety impossible. There are few variations of what I did but if you copy this code and paste it on Typescript Playground you will see that without using modules it works perfectly fine. With modules I stepped in the territory of Inferring nested generic types, which turned out to be a real challenge and it doesn't really work (more related to modules tree, see below).

Another thing to note is that the Vue type augmentation performed creates a store as any, which regardless of any work we could do with the typing system would make the $store in components act as any for operations like commit, dispatch, etc. We can't really do that without adding extra parameters on Vue core types, but we don't want to do that because Vue would know about and be couple with Vuex. Maybe we could leave the augmentation to the project level so the specific generics could be passed in? I'm not sure what would be the best way around this, need to research more.

Honestly, at this moment, with my limited knowledge, I would say only rewriting chunks of Vuex would allow us to have decent TS types.

Here is what I got, that might be helpful in case somebody is giving a thought to this:
(I had to copy the basic Options type so it compiles fine, this is just a small set of the types directly related to the Store object)

export interface ModuleOptions{
  preserveState?: boolean
}

export interface WatchOptions {
  deep?: boolean;
  immediate?: boolean;
}

export interface DispatchOptions {
  root?: boolean;
}

export interface CommitOptions {
  silent?: boolean;
  root?: boolean;
}

export interface Payload {
  type: string;
}

export interface Payload {
  type: string;
}

export interface MutationPayload extends Payload {
  payload: any;
}

export interface Dispatch<Actions> {
  <T>(type: keyof Actions, payload?: T, options?: DispatchOptions): Promise<any>;
  <P extends Payload>(payloadWithType: P, options?: DispatchOptions): Promise<any>;
}

export interface Commit<Mutations> {
  <T>(type: keyof Mutations, payload?: T, options?: CommitOptions): void;
  <P extends Payload>(payloadWithType: P, options?: CommitOptions): void;
}

export type Mutation<State> = <Payload>(state: State, payload?: Payload) => any;

export type MutationTree<State, Mutations> = {
  [K in keyof Mutations]: Mutation<State>
}

export type Getter<State, RootState, Getters, RootGetters> = (
  state: State,
  getters: Getters,
  rootState: RootState,
  rootGetters: RootGetters
) => any;

export type GetterTree<State, RootState, Getters> = {
  [K in keyof Getters]: Getter<State, RootState, Getters[K], Getters>
}

export interface ActionContext<State, RootState, Getters, Mutations, Actions> {
  dispatch: Dispatch<Actions>;
  commit: Commit<Mutations>;
  state: State;
  getters: Getters;
  rootState: RootState;
  rootGetters: any;
}

type ActionHandler<State, RootState, Getters, Mutations, Actions> = <Payload>(
  injectee: ActionContext<State, RootState, Getters, Mutations, Actions>,
  payload: Payload
) => any;

interface ActionObject<State, RootState, Getters, Mutations, Actions> {
  root?: boolean;
  handler: ActionHandler<State, RootState, Getters, Mutations, Actions>;
}

export type Action<State, RootState, Getters, Mutations, Actions> =
  | ActionHandler<State, RootState, Getters, Mutations, Actions>
  | ActionObject<State, RootState, Getters, Mutations, Actions>;

export type ActionTree<State, RootState, Getters, Mutations, Actions> = {
  [K in keyof Actions]: Action<State, RootState, Getters, Mutations, Actions>
}

export interface Module<State, Getters, Mutations, Actions, Modules> {
  namespaced?: boolean;
  state?: State | (() => State);
  getters?: GetterTree<State, any, Getters>;
  mutations?: MutationTree<State, Mutations>;
  actions?: ActionTree<State, any, Getters, Mutations, Actions>;
  modules?: ModuleTree<Modules>;
}

export type ModuleTree<Modules> = {
  [K in keyof Modules]: Module<
    Modules[K],
    Modules[K],
    Modules[K],
    Modules[K],
    Modules[K]
  >
}

export type Plugin<State, Mutations, Getters, Actions, Modules> = (store: Store<State, Mutations, Getters, Actions, Modules>) => any;

export interface StoreOptions<State, Mutations, Getters, Actions, Modules> {
  state?: State;
  getters?: GetterTree<State, State, Getters>
  mutations?: MutationTree<State, Mutations>;
  actions?: ActionTree<State, State, Getters, Mutations, Actions>;
  modules?: ModuleTree<Modules>;
  plugins?: Plugin<State, Mutations, Getters, Actions, Modules>[];
  strict?: boolean;
}

class Store<State, Mutations, Getters, Actions, Modules> {
  constructor(options: StoreOptions<State, Mutations, Getters, Actions, Modules>) { 
      
  };

  readonly state: State;
  // trying to add this to test whether we could at least 
  // get the types from a regular modules property
  readonly modules: Modules;
  readonly getters: Getters;

  replaceState: (state: State) => void;

  commit: Commit<Mutations>;
  dispatch: Dispatch<Actions>;

  subscribe: <P extends MutationPayload>(fn: (mutation: P, state: State) => any) => () => void;
  watch: <T>(getter: (state: State) => T, cb: (value: T, oldValue: T) => void, options?: WatchOptions) => () => void;

  registerModule: <
    ModuleState,
    ModuleGetters,
    ModuleMutations,
    ModuleActions,
    ModuleModules extends ModuleTree<ModuleModules>
  >(path: string, module: Module<ModuleState, ModuleGetters, ModuleMutations, ModuleActions, ModuleModules>, options?: ModuleOptions) => void;

  registerModulePath: <
    ModuleState,
    ModuleGetters,
    ModuleMutations,
    ModuleActions,
    ModuleModules extends ModuleTree<ModuleModules>
  >(path: string[], module: Module<ModuleState, ModuleGetters, ModuleMutations, ModuleActions, ModuleModules>, options?: ModuleOptions) => void;

  // this could be type safe as well, since we're dealing with existing paths
  // but i didn't bother since the whole module concept isn't working :(
  unregisterModule: (path: string) => void;
  unregisterModulePath: (path: string[]) => void;

}

const simpleStoreObject = new Store({
  state: {
    count: 0,
    countString: '1'
  },
  mutations: {
      decrement: (state) => state.count,
  },
  getters: {
    isCountAt10: (state): boolean => { return state.count === 10 }
  },
  actions: {
    decrementAsync: (context) => {
      setTimeout(() => { context.commit('decrement') }, 0);
    }
  }
});

simpleStoreObject.state.count;
simpleStoreObject.state.countString;
simpleStoreObject.state.error;
simpleStoreObject.commit('increment');
simpleStoreObject.commit('decrement');
simpleStoreObject.getters.isCountAt10;
simpleStoreObject.getters.iDontExist;
simpleStoreObject.replaceState({ count: 1, countString: '123123' });
simpleStoreObject.replaceState({ count: '1', countString: 123123 });
simpleStoreObject.dispatch('incrementAsync');
simpleStoreObject.dispatch('decrementAsync');
simpleStoreObject.modules.a.state.propA;

// nothing works for modules
// if I had a Pick in TS that instead of returning me an object that
// contains the property, I could just get the value of that property
// maybe it'd work

const fractalStoreObject = new Store({
  modules: {
    a: {
      state: {
        propA: '123'
      },
      mutations: {
        updatePropA: (state, payload) => state.propA = payload
      },
      getters: {
        isPropA123: (state): boolean => state.propA === '123'
      }
    },
    b: {
      state: {
        propB: 123
      },
      mutations: {
        updatePropB: (state, payload) => state.propB = payload
      },
      getters: {
        isPropB123: (state): boolean => state.propB === 123
      }
    }
  }
});
@tiangolo

This comment has been minimized.

Copy link

@tiangolo tiangolo commented Nov 11, 2018

About the discussion of adding RxJS support, let me add my two cents:

One of the main reasons I switched to Vue after having worked a lot in Angular is that it doesn't force Observables everywhere when they are not absolutely required (it doesn't even mention them).

I have worked what I think is quite a lot with RxJS, I have even made contributions to NgRX (the Redux-like Angular equivalent to Vuex) and PRs to Angular itself. And after working heavily with RxJS, I found out that it ends up being more useful only when absolutely necessary. For many cases, it ended up being a very complex overkill. Very difficult to debug, as you can't use them directly (e.g. in the browser console) but you have to create functions that you pass to RxJS' operators and then wait for them to do their job. At that point debugging ends up having to be with just console.logs, instead of being able to explore stuff live. And the operators, as powerful as they are, are not necessarily very intuitive. The differences between many of the operators are very subtle.

For me, the only clear example of the advantage of using Observables over alternatives was HTTP API throttling or "debouncing", and that is achieved in the official Vue.js guide in a simpler way with just lodash.

But my main point is, as powerful as RxJS is, it's quite complex, and now I suggest not using it unless it's very clear what advantage it's actually providing or which problem it is actually solving better.

Adding a layer of RxJS that exposes the store as Observables might be a very good idea for an external package, so that developers that have a strong requirement for RxJS can use it.

But I would recommend against using it as the main Vuex interface for all the developers.

@championswimmer

This comment has been minimized.

Copy link

@championswimmer championswimmer commented Nov 19, 2018

So I had been working a library of late, (which is picking a bit of traction, like few hundred downloads a day). The aim wasn't exactly to replace/rewrite Vuex with types but add a Typed wrapper, pretty similar to what vue-class-component and vue-property-decorators does with Vue components.

https://github.com/championswimmer/vuex-module-decorators

It heavily depends on you describing your store in form of modules, and each module is then created like a ES6 class (syntactic sugar of decorators - which basically just create the prototype of the class as the old-school vuex module object).
The advantage is, we can describe -

  • state fields simply as class fields
  • getters as simply ES6 get proxies
  • mutations as functions (needs the @Mutation decorator)
  • actions as async functions (needs the @Action decorator)

Since the module is an ES6 class, the fields, getters, mutations and actions are all described as fields and methods of the class - giving them 100% type safety, autocomplete, refactoring support.

Modules are written somewhat like this

import {Module, VuexModule, Mutation, Action} from 'vuex-module-decorators'

@Module
export default class Counter2 extends VuexModule {
  count = 0

  @Mutation increment(delta: number) {this.count+=delta}
  @Mutation decrement(delta: number) {this.count-=delta}

  // action 'incr' commits mutation 'increment' when done with return value as payload
  @Action({commit: 'increment'}) incr() {return 5}
  // action 'decr' commits mutation 'decrement' when done with return value as payload
  @Action decr() {this.decrement(5)}
}

Accessing this module from other components works this way

import { getModule } from 'vuex-module-decorators'
import Counter2 from '@/store/modules/counter2'

const c2 = getModule(Counter2)

await c2.incr() // call directly actions

c2.decrement(1) // call directly mutations

console.log(c2.count) // access state fields
@sascha245

This comment has been minimized.

Copy link

@sascha245 sascha245 commented Nov 23, 2018

I also started working on a solution for vuex typings and came out with something pretty similar to @championswimmer, though still not as mature: vuex-simple
In the new version I brought out not to long ago there are however some interesting and unique improvements:

  • We just write normal typescript classes. This means that we can use everything that would normally be possible, which also includes inheritance and generics. The decorators don't apply any logic and just store some metadata used later on by the library.
class FooModule {
  @State()
  public counter: number;

  constructor(nb: number = 0) {
    this.counter = nb;
  }

  @Mutation()
  public increment() {
    this.counter++;
  }
}
  • We can have multiple module instances if necessary. The above would give us two submodules 'foo1' and 'foo2' with different initial values.
class BarModule {
  @Module()
  public foo1 = new FooModule(5);

  @Module()
  public foo2 = new FooModule(10);

  @Getter()
  public get total() {
    return this.foo1.counter + this.foo2.counter;
  }
}
  • We can also easily create multiple stores from our defined module classes:
const bar1 = new BarModule();
// transforms and binds 'bar1' to the created store
const store1 = createVuexStore(bar1, {
  strict: true,
  modules: {}, // you can still add normal vuex modules
  plugins: []
})
// we can then call our mutations, getters and such as we normally would with our class instance
bar1.foo1.increment();
console.log('bar1.foo1.counter', bar1.foo1.counter); // 6

// another store
const bar2 = new BarModule();
const store2 = createVuexStore(bar2)
console.log('bar2.foo1.counter', bar2.foo1.counter); // 5
@xqdoo00o

This comment has been minimized.

Copy link

@xqdoo00o xqdoo00o commented Jan 2, 2019

Any official news in 2019?

@TotomInc

This comment has been minimized.

Copy link

@TotomInc TotomInc commented Jan 2, 2019

@xqdoo00o IIRC, vuex is kinda limited with TypeScript because of vue core API which doesn't provide enough types for TypeScript users - types are actually built with flow.

Vue 3.0 will switch from flow to TypeScript for the internal API which will make things much easier for vuex (and other vue-based modules with TypeScript).

Here are slides from Evan You at Vue Toronto Conference announcing what is coming for Vue 3.0.

You can also take a look at the roadmap on vuejs/vue GitHub projects, which have been preferred instead of the traditional roadmap repo now archived.

The 3.0 alpha release will maybe be released in Q1 2019, as it is written in the roadmap.

@sascha245

This comment has been minimized.

Copy link

@sascha245 sascha245 commented Mar 16, 2019

So, I finally managed to get full type inference for standard vuex module's state/getters/dispatch/commit without any additional boilerplate. You can look at it here:
https://github.com/sascha245/vuex-context

In the end, there isn't much code but getting it to work correctly was a lot more complicated than I thought...

@mrcrowl

This comment has been minimized.

Copy link

@mrcrowl mrcrowl commented Mar 16, 2019

Nice succinct solution @sascha245! Going to be trying this out soon. Is it still worth mentioning in 2019 that it requires ES6?

@mrcrowl

This comment has been minimized.

Copy link

@mrcrowl mrcrowl commented Mar 16, 2019

Also see https://github.com/victorgarciaesgi/Vuex-typed-modules for yet another approach. Looks like we're all hanging out for Vue 3.

@sascha245

This comment has been minimized.

Copy link

@sascha245 sascha245 commented Mar 16, 2019

@mrcrowl Thanks! And yes my solution makes use of ES6 Proxies, though that shouldn't be too big of a problem nowadays as even Vue is going to use them in the next version ^^

I didn't know about this library yet, but it's looks like we got similar ideas concerning the typings. Though thanks to it I found one thing I could improve on my typings for the getters.

@pumpkinlink

This comment has been minimized.

Copy link

@pumpkinlink pumpkinlink commented Mar 25, 2019

Any official news on how this will probably be implemented on Vuex 4?

Seems that vuex-module-decorators is the most well-maintained workaround for now, but I'd like to at least know if the official solution will be somewhat similar to that one / vuex-simple (class-based store definition), or vuex-context / vuex-type-helper (object-based store with generated typed helpers).

That way me and my team can more easily migrate to Vue 3 / Vuex 4 in the future.

Thank you in advance

@mrkswrnr

This comment has been minimized.

Copy link

@mrkswrnr mrkswrnr commented Apr 9, 2019

I also started working on yet another library for writing ES6/typescript classes as vuex modules without having to use decorators but with full type completion support. Also it is possible to write mutators (which I hope will be removed in vuex 4) as ES6 setter functions: vuex-typesafe-class.

I hope vuex 4 will be close to that (except the need to use mutators).

@garyo

This comment has been minimized.

Copy link

@garyo garyo commented Apr 13, 2019

I'm also starting on a fairly large Vue/Vuex typescript app. I'm trying to figure out best practices for dir layout and module structure. I think this makes sense:

store/
  store.ts
  store-types.ts
  moduleA.ts
  moduleA-types.ts

but if I'm going to have submodules, maybe deeper nesting is best:

store/
  store.ts
  store-types.ts
  moduleA/
    moduleA.ts
    moduleA-types.ts
    subA/
      subA.ts
      subA-types.ts
  moduleB/
    moduleB.ts
    moduleB-types.ts

(I don't like lots of files named 'index.ts'; they all look the same. I prefer files to be named for what they are.)
So does this latter structure look good? It seems like you do need a foo-types.ts for each module so the Vue components can import the module's shape, right? (I suppose you could put all types in store/store-types.ts though -- they don't create any Javascript output, right?)
It does seem overly deeply nested to me; I don't like adding extra dir layers unless needed.
Is there a "best practices" document on how to do this?

@ClickerMonkey

This comment has been minimized.

Copy link

@ClickerMonkey ClickerMonkey commented Apr 16, 2019

I tackled it here - with no additional code, just better typing:

https://github.com/ClickerMonkey/vuex-typescript-interface

I started where @danielfigueiredo did, but figured out I could automatically detect getters, mutations, and actions all from one interface.

So now state, getters, commit, and dispatch all require valid strings and types or TS will throw an error. If you also have state/getters/actions/mutations defined on your interface and NOT in your store it will throw an error. The getter/mutation/action definitions passed in as options must have the proper types or it will also throw an error here as well!

@jdriesen

This comment has been minimized.

Copy link

@jdriesen jdriesen commented Oct 6, 2019

Hi All,

Super interesting comments.
Thank you all !

If I can ask ... (we're now October 2019 and we're all using Vue 3) ...
Any official news on this subject ?
In other words ... What's the proper way to implement Vuex Typescript in a Vue 3 Project ?

Regards,
Johnny

@garyo

This comment has been minimized.

Copy link

@garyo garyo commented Oct 6, 2019

I don't know about Vue 3 (I'm waiting for the official release) but I do have a reasonably clean, complete Typescript-typesafe vuex-module-decorators example at https://github.com/garyo/vuex-module-decorators-example.git if you're interested.

@paleo

This comment has been minimized.

Copy link

@paleo paleo commented Oct 13, 2019

I found a solution for the code that uses the store and calls dispatch, commit, getters and the state. For example:

store.dispatch("myModule/myAction", myPayload);

… is replaced by a wrapper:

store.dispatch.myModule.myAction(myPayload);

… which is fully typed.

https://github.com/paleo/direct-vuex

@Kriget

This comment has been minimized.

Copy link

@Kriget Kriget commented Dec 6, 2019

this worked for me:

// in vuexCommitFuncVars.ts
export default {
  decreaseCount: "decreaseCount",
  ...
};

// in actions.ts
import funcVars from "./vuexCommitFuncVars"; 
commit(funcVars.decreaseCount);
// before i had to write 'commit("decreaseCount")'

// in mutations.ts
[funcVars.decreaseCount](state) {
  state.count= state.count- 1;
},

@AlenQi

This comment has been minimized.

Copy link

@AlenQi AlenQi commented Jan 9, 2020

Any official news in 2020?

@TotomInc

This comment has been minimized.

Copy link

@TotomInc TotomInc commented Jan 9, 2020

IIRC, vue next (aka vue 3) is on an alpha release, we still need to wait for the official vue ecosystem to update, and in the future this will (most probably) entirely handled natively by vuex.

Please correct me if I’m wrong.

@MikeMitterer

This comment has been minimized.

Copy link

@MikeMitterer MikeMitterer commented Jan 9, 2020

@AlenQi I'm using Vue 2 and Vuex with TS without any problems.
Here you can see how I use it: https://github.com/MikeMitterer/vuetify-ts-starter/blob/master/src/views/About.vue (CounterStore)
And here
https://github.com/MikeMitterer/vuetify-ts-starter/tree/master/src/store
how everything is glued together. Hope this helps.

@viT-1

This comment has been minimized.

Copy link

@viT-1 viT-1 commented Jan 15, 2020

@AlenQi
Vue 2 typescript examples with vuex & without webpack/rollup (tsc transpiling & bundling):

  • simple with SystemJs module resolving only
  • tough with esm & SystemJs support, eslint & jest (gulp-replace for esm modules resolving)
@F0rsaken

This comment has been minimized.

Copy link

@F0rsaken F0rsaken commented Mar 16, 2020

So I made a small library wich allows you to get fully typed store in actions and components.
It also adds helpers that allow you to import rather than us mapXXX or decorators

https://github.com/owlsdepartment/vuex-typed

@gcollombet

This comment has been minimized.

Copy link

@gcollombet gcollombet commented Mar 26, 2020

For me adding the store state type in module declaration is enough to have auto-completion

declare module "vue/types/vue" {
  interface Vue {
    $store: Store<RootState| any>;
  }
}

A more complete exemple :

import Vue from "vue"
import Vuex, { StoreOptions} from "vuex"
import App from './App.vue'

Vue.use(Vuex)

interface RootState {
  user: App.Models.User | null;
  notifications: App.Models.Notification[];
}

const store: StoreOptions<RootState> = {
  state: {
    user: null,
    notifications: []
  },
  mutations: {
    login(state, user: User) {
      state.user = user
    },
    logout(state) {
      state.user = null
    }
  },
  actions: {
  },
  modules: {
  }
}

store = new Vuex.Store<RootState>(store);

declare module "vue/types/vue" {
  interface Vue {
    // https://www.typescriptlang.org/docs/handbook/declaration-files/do-s-and-don-ts.html#use-union-types
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    $store: Store<RootState| any>;
  }
}

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

You can’t perform that action at this time.