vuejs / vuex

πŸ—ƒοΈ Centralized State Management for Vue.js.
https://vuex.vuejs.org
MIT License
28.42k stars 9.58k forks source link

Write Stores in Typescript #564

Open Anonyfox opened 7 years ago

Anonyfox commented 7 years ago

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 commented 7 years ago

/ping @ktsn

ktsn commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

@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 commented 7 years ago

@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 commented 7 years ago

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 commented 7 years ago

@Anonyfox wow! thank you very much! I adopted your example into my environment and got it working πŸ‘

Glidias commented 7 years ago

Related: https://github.com/vuejs/vuex/issues/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 commented 7 years ago

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 commented 7 years ago

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

wonderful-panda commented 7 years ago

@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 commented 7 years ago

@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 commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

@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 commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

@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:

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 commented 7 years ago

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 commented 7 years ago

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 commented 7 years ago

@istrib do you have gist example for reference please?

istrib commented 7 years ago

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

ghost commented 7 years ago

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 commented 7 years ago

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

ghost commented 7 years ago

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

sandangel commented 7 years ago

Since vue 2.5 has released with full typescript supported, I just want to know which is the official implementation of vuex in typescript, vuex-typescript or vuex-typex or something else?. I think having an example in the official page will help.

LinusBorg commented 7 years ago

I just want to know which is the official implementation of vuex in typescript, vuex-typescript or vuex-typex or something else?

The short, probably disappointing answer is: There's not official implementation.

Vue's 2.5 Typescript improvement apply to Vue, not vuex.

@ktsn Do you have a recommendation?

ktsn commented 7 years ago

@LinusBorg @sandangel I actually haven't had experiences with such wrappers for type safety in Vuex. But I'm thinking about improving Vuex typings that would work well with Vue v2.5 typings. The fundamental idea can be seen here. https://github.com/ktsn/vuex-type-helper

sandangel commented 7 years ago

how about rxjs powered vuex store with typescript? It would be great if Vuex team can considerate rewriting this library with rxjs and type safe in mind like @ngrx.

LinusBorg commented 7 years ago

rewriting ... with rxjs

if at all, this would only be happening as an optional extension, because rx is far too heavy for an otherwise lightweight library like vuex.

amritk commented 7 years ago

Here's a nice tutorial on it https://egghead.io/courses/vue-js-state-management-with-vuex-and-typescript

sandangel commented 7 years ago

@LinusBorg what i mean when saying "rxjs in mind" is the idea thinking data as a stream. Observable is now at stage 1 in ECMAScript proposals and will be become native javascript soon, which mean you don't have to use rxjs to create observable, you just have to expose Getters, Actions as observable, import some operators and lettable operator in rxjs 5.5 will keep library lightweight. IMO, it 's hard to achieve that goal with current design/architecture of vuex, so a full rewrite is needed.

szwacz commented 7 years ago

Somehow I can't get this.$store.state to be of type MyState and not any. Does anyone got it working?

mitchell-garcia commented 7 years ago

@szwacz it's being discussed here: https://github.com/vuejs/vuex/issues/994. Because Vuex includes typings for this.$store, you can't overwrite them on a project-basis. You also can't import specific properties from Vuex and define this.$store yourself, because the file that imports Vuex actually does this overwriting.

The only way I could get it working was copying the types included in Vuex in my directory and telling the compiler to use my project's file, not Vuex's. With a tsconfig.json like this:

//tsconfig.json
{
  "compilerOptions": {
      "baseUrl": "./",
      "paths": {
        "vuex": ["typings/vuex.d.ts"]
      }
      ...
   }
}

and a typings/vuex.d.ts file that looks like this: https://gist.github.com/mmitchellgarcia/af540bd0bcb3a734f2c35287584afad3.

You should get Intellisense/static-typing for this.$store.state

It will go out of sync when there's version update; so this is not a long-term solution. I believe this issue will be resolved shortly, so I hope it helps for now!

zetaplus006 commented 7 years ago

This is my attempt. Welcome to trial and suggestion. https://github.com/zetaplus006/vubx

bmingles commented 6 years ago

If you are using TypeScript 2.1+, you can take advantage of keyof and mapped types to get type safety for your commit and dispatch calls (both for the string identifiers and their payloads). It only involves types and doesn't require any additional runtime code. Here's a gist that shows the idea.

https://gist.github.com/bmingles/8dc0ddcb87aeb092beb5a12447b10a36

ktsn commented 6 years ago

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

danielfigueiredo commented 6 years ago

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 commented 6 years ago

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 commented 6 years ago

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 -

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 commented 5 years ago

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:

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

xqdoo00o commented 5 years ago

Any official news in 2019?

TotomInc commented 5 years ago

@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 commented 5 years ago

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 commented 5 years ago

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

mrcrowl commented 5 years ago

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

sascha245 commented 5 years ago

@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.