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 · 41 comments

Comments

Projects
None yet
@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.

Show comment
Hide comment
@LinusBorg

LinusBorg Jan 4, 2017

Member

/ping @ktsn

Member

LinusBorg commented Jan 4, 2017

/ping @ktsn

@ktsn

This comment has been minimized.

Show comment
Hide comment
@ktsn

ktsn Jan 4, 2017

Member

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

Member

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.

Show comment
Hide comment
@Anonyfox

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

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.

Show comment
Hide comment
@ktsn

ktsn Jan 4, 2017

Member

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.

Member

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.

Show comment
Hide comment
@Anonyfox

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

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.

Show comment
Hide comment
@morhi

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

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.

Show comment
Hide comment
@Anonyfox

Anonyfox 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

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.

Show comment
Hide comment
@wonderful-panda

wonderful-panda 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.

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.

Show comment
Hide comment
@morhi

morhi Jan 19, 2017

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

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.

Show comment
Hide comment
@Glidias

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

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.

Show comment
Hide comment
@wonderful-panda

wonderful-panda 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.

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.

Show comment
Hide comment
@Glidias

Glidias Jan 26, 2017

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

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.

Show comment
Hide comment
@wonderful-panda

wonderful-panda 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` */ });

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.

Show comment
Hide comment
@Glidias

Glidias 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

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.

Show comment
Hide comment
@snaptopixel

snaptopixel Feb 16, 2017

Contributor

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.

Contributor

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.

Show comment
Hide comment
@ktsn

ktsn Feb 16, 2017

Member

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 🙂

Member

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.

Show comment
Hide comment
@snaptopixel

snaptopixel Feb 16, 2017

Contributor

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

Contributor

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.

Show comment
Hide comment
@chanon

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

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.

Show comment
Hide comment
@snaptopixel

snaptopixel Feb 19, 2017

Contributor

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.

Contributor

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.

Show comment
Hide comment
@istrib

istrib 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);

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.

Show comment
Hide comment
@mrcrowl

mrcrowl 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()
})

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.

Show comment
Hide comment
@Shepless

Shepless 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).

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.

Show comment
Hide comment
@istrib

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

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.

Show comment
Hide comment
@Shepless

Shepless Jun 20, 2017

@istrib do you have gist example for reference please?

Shepless commented Jun 20, 2017

@istrib do you have gist example for reference please?

@istrib

This comment has been minimized.

Show comment
Hide comment
@istrib

istrib Jun 21, 2017

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

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.

Show comment
Hide comment
@michaelharrisonroth

michaelharrisonroth 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

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.

Show comment
Hide comment
@DevoidCoding

DevoidCoding 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

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.

Show comment
Hide comment
@michaelharrisonroth

michaelharrisonroth Aug 7, 2017

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

michaelharrisonroth commented Aug 7, 2017

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

@sandangel

This comment has been minimized.

Show comment
Hide comment
@sandangel

sandangel Oct 19, 2017

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.

sandangel commented Oct 19, 2017

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

This comment has been minimized.

Show comment
Hide comment
@LinusBorg

LinusBorg Oct 19, 2017

Member

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?

Member

LinusBorg commented Oct 19, 2017

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

This comment has been minimized.

Show comment
Hide comment
@ktsn

ktsn Oct 19, 2017

Member

@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

Member

ktsn commented Oct 19, 2017

@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

This comment has been minimized.

Show comment
Hide comment
@sandangel

sandangel Oct 19, 2017

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.

sandangel commented Oct 19, 2017

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

This comment has been minimized.

Show comment
Hide comment
@LinusBorg

LinusBorg Oct 19, 2017

Member

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.

Member

LinusBorg commented Oct 19, 2017

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

This comment has been minimized.

Show comment
Hide comment

amritk commented Oct 19, 2017

@sandangel

This comment has been minimized.

Show comment
Hide comment
@sandangel

sandangel Oct 20, 2017

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

sandangel commented Oct 20, 2017

@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

This comment has been minimized.

Show comment
Hide comment
@szwacz

szwacz Oct 25, 2017

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

szwacz commented Oct 25, 2017

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

@mmitchellgarcia

This comment has been minimized.

Show comment
Hide comment
@mmitchellgarcia

mmitchellgarcia Oct 25, 2017

@szwacz it's being discussed here: #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!

mmitchellgarcia commented Oct 25, 2017

@szwacz it's being discussed here: #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

This comment has been minimized.

Show comment
Hide comment
@zetaplus006

zetaplus006 Nov 20, 2017

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

zetaplus006 commented Nov 20, 2017

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

@bmingles

This comment has been minimized.

Show comment
Hide comment
@bmingles

bmingles Dec 12, 2017

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

bmingles commented Dec 12, 2017

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 ktsn referenced a pull request that will close this issue Jan 4, 2018

Open

feat: improve helper types for more type safety #1121

@ktsn

This comment has been minimized.

Show comment
Hide comment
@ktsn

ktsn Jan 4, 2018

Member

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

Member

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.

Show comment
Hide comment
@danielfigueiredo

danielfigueiredo 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
      }
    }
  }
});

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
      }
    }
  }
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment