Skip to content

Instantly share code, notes, and snippets.

@javisperez
Created August 11, 2020 15:13
Show Gist options
  • Save javisperez/b13d02042620ae663f0a1f81b050ca69 to your computer and use it in GitHub Desktop.
Save javisperez/b13d02042620ae663f0a1f81b050ca69 to your computer and use it in GitHub Desktop.
import { createStore } from 'vuex';
// The modules to import
import auth, { Store as AuthStore, State as AuthState } from './auth';
import counter, { Store as CounterStore, State as CounterState } from './counter';
// A State type with all the submodules
type State = {
auth: AuthState;
counter: CounterState;
}
// And the store is an extension of all the stores but each store receives its own state
export type Store = AuthStore<Pick<State, 'auth'>> & CounterStore<Pick<State, 'counter'>>;
const store = createStore({
modules: {
auth,
counter,
},
});
export function useStore() {
// This made the trick to me, i dont love it, but i dont know other way to do it.
// The compiler kept complaining about it not being compatible with Vuex' store so i casted it to uknnown
// and after that re-casted as the Store we just defined.
return store as unknown as Store;
}
export default store;
@noque-lind
Copy link

I'm getting an error following this solution: 'Store' is not generic in line 14. Any tips, or possibly source code?

Thanks.

@javisperez
Copy link
Author

javisperez commented Sep 2, 2020

Ah yeah, that's on me, sorry, the stores (like AuthStore or CounterStore) needs to receive a State generic to pass, like this:

type Store<S = State> = Omit<VuexStore<S>, 'getters' | 'commit' | 'dispatch'> & {
   ...
}

I forgot that part, will update the gist in a bit.

I'm getting an error following this solution: 'Store' is not generic in line 14. Any tips, or possibly source code?

Thanks.

@MahmoudMm
Copy link

Can you post a full example of this solution !?

@TrayHard
Copy link

TrayHard commented Sep 7, 2020

Would be much appreciated if you will post full example, I mean, what should be in that counter.ts and auth.ts files? Also is it possible to have some common state, mutations and actions or only module ones?

@soerenmartius
Copy link

soerenmartius commented Sep 15, 2020

This is how I managed to get it working:

/src/store/index.ts

import { createStore, createLogger } from 'vuex'
import createPersistedState from 'vuex-persistedstate'

import {
  AuthModule,
  Store as AuthStore,
  State as AuthState,
} from '@/modules/auth/store'

import {
  DomainModule,
  Store as DomainStore,
  State as DomainState,
} from '@/modules/domain/store'

export type State = {
  auth: AuthState
  domain: DomainState
}

export type Store = AuthStore<Pick<State, 'auth'>> &
  DomainStore<Pick<State, 'domain'>>

export const store = createStore({
  plugins: [createLogger(), createPersistedState()],
  modules: { AuthModule, DomainModule },
})

export function useStore(): Store {
  return store as Store
}

export default store

/src/modules/domain/store/index.ts

import {
  ActionContext,
  ActionTree,
  GetterTree,
  Store as VuexStore,
  CommitOptions,
  DispatchOptions,
  MutationTree,
  Module,
} from 'vuex'

import { State as RootState } from '@/store'

import DomainService, { ResponseDomain as Domain } from '@/services/domain'
import RedirectionService, {
  ResponseRedirection as Redirection,
} from '@/services/redirection'

// Declare state
export type State = {
  domains: Domain[]
  redirections: Redirection[]
}

// Create initial state
const state: State = {
  domains: [],
  redirections: [],
}

// mutations enums
export enum MutationTypes {
  SET_DOMAINS = 'SET_DOMAINS',
  SET_REDIRECTIONS = 'SET_REDIRECTIONS',
}

// Mutation contracts
export type Mutations<S = State> = {
  [MutationTypes.SET_DOMAINS](state: S, domains: Domain[]): void
  [MutationTypes.SET_REDIRECTIONS](state: S, redirections: Redirection[]): void
}

// Define mutations
const mutations: MutationTree<State> & Mutations = {
  [MutationTypes.SET_DOMAINS](state: State, domains: Domain[]) {
    state.domains = domains
  },
  [MutationTypes.SET_REDIRECTIONS](state: State, redirections: Redirection[]) {
    state.redirections = redirections
  },
}

// Action enums
export enum ActionTypes {
  FETCH_DOMAINS = 'FETCH_DOMAINS',
  FETCH_REDIRECTIONS = 'FETCH_REDIRECTIONS',
}

// Actions context
type AugmentedActionContext = {
  commit<K extends keyof Mutations>(
    key: K,
    payload: Parameters<Mutations[K]>[1],
  ): ReturnType<Mutations[K]>
} & Omit<ActionContext<State, RootState>, 'commit'>

// Actions contracts
export interface Actions {
  [ActionTypes.FETCH_DOMAINS](
    { commit }: AugmentedActionContext,
    teamId: string,
  ): void
  [ActionTypes.FETCH_REDIRECTIONS](
    { commit }: AugmentedActionContext,
    payload: { teamId: string; domainName: string },
  ): void
}

// Define actions
export const actions: ActionTree<State, RootState> & Actions = {
  async [ActionTypes.FETCH_DOMAINS]({ commit }, teamId: string) {
        // your logic here
  },

  async [ActionTypes.FETCH_REDIRECTIONS](
    { commit },
    payload: { teamId: string; domainName: string },
  ) {
    // your logic here
}

// getters types
export type Getters = {
  getDomains(state: State): Domain[]
  getRedirections(state: State): Redirection[]
}

// getters
export const getters: GetterTree<State, RootState> & Getters = {
  getDomains: (state) => {
    return state.domains
  },
  getRedirections: (state) => {
    return state.redirections
  },
}

//setup store type
export type Store<S = State> = Omit<
  VuexStore<S>,
  'commit' | 'getters' | 'dispatch'
> & {
  commit<K extends keyof Mutations, P extends Parameters<Mutations[K]>[1]>(
    key: K,
    payload: P,
    options?: CommitOptions,
  ): ReturnType<Mutations[K]>
} & {
  getters: {
    [K in keyof Getters]: ReturnType<Getters[K]>
  }
} & {
  dispatch<K extends keyof Actions>(
    key: K,
    payload: Parameters<Actions[K]>[1],
    options?: DispatchOptions,
  ): ReturnType<Actions[K]>
}

export const DomainModule: Module<State, RootState> = {
  state,
  mutations,
  actions,
  getters,
  // namespaced: true,
}

hope that helps :)

@ux-engineer
Copy link

Thanks @soerenmartius. I've made a full example repo and a codesandbox, referenced in this SO question, as there are still a few quirks that needs to be solved...

Hoping for any further input from you guys!

@javisperez
Copy link
Author

Sorry about the delay guys, its been crazy busy for me and I haven't got the time to make a full demo, sorry about that.

I'm glad that there's some solutions posted here though, thank you, I've had planned a codesandbox demo but i think @ux-engineer just covered it, that was basically the same idea i had. The only thing i didnt fully understood with your approach @ux-engineer was why are you passing the whole RootState to the AugmentedActionContext in your actions, if you wont be using other module's states? like the FETCH_DOCUMENTS action doesn't use the state you could just pass the DocumentsState and keep it contained in the module?

Maybe I'm missing something but anyway, i think is a great job, thanks for doing it and thanks for sharing it!

@ux-engineer
Copy link

@javisperez that RootState type seems to be needed to be imported in modules index, getters, and actions files. @soerenmartius had used this syntax also.

However, it's resulting in cyclical dependency linting error, so there is some quirk about it. But somehow it works 🤸‍♀️ ...

If using <GetterTree<State, State> & Getters, I'm getting this type error:

Conversion of type 'Store<State>' to type 'DocumentsStore<Pick<RootState, "documents">>' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.

On this return statement:

export function useStore(): Store {
  return store as Store;
}

@javisperez
Copy link
Author

@ux-engineer ahh, in my case I solved that conversion error with this return store as unknown as Store instead of return store as Store, based on what the same error says "If this was intentional, convert the expression to 'unknown' first" and that worked for me.

@javisperez
Copy link
Author

oh and also, about the RootState that's fine, I just wouldn't do it if I won't be accessing cross-modules states, if everything in my actions is going to belong to my module's state I'd just use the State but that's just me, I don't think its a bad thing.

@ux-engineer
Copy link

ux-engineer commented Sep 26, 2020

@javisperez its true that this typing problem could be circumvented with type-casting to unknown first.

However, this double assertion casting via unknown is a way of forcefully casting any type to any other type. After doing so, as far as I understand (and am not a deep expert in TS), there could be something wrong with our store or with the library, which TypeScript might not be recognizing as an error.

So it doesn't seem a correct solution because there seems to be something wrong with typings in the first place.

@javisperez
Copy link
Author

@ux-engineer yeah, i think you're right, but sadly i don't really have the TS experience to make a proper fix without including the whole RootState there. I mean, I'm really a TS noob 😬

@ux-engineer
Copy link

ux-engineer commented Sep 26, 2020

No worries, we are using community effort here...if this remains to be unresolved, I will open up an issue to Vuex repository and/or write to core team members in Discord channel 😁

Vue 3 and Vue Router 4 (in beta) are written fully in TypeScript, however, Vuex 4 is not. It's TypeScript rewrite is coming up in Vuex 5 later on. But they have been working towards better TS support already with this version 4, which is still in beta.

@ux-engineer
Copy link

Here's an issue thread in Vuex repo related to using modules in Vuex 4.0: vuejs/vuex#1833

@ux-engineer
Copy link

ux-engineer commented Sep 27, 2020

I just realized VS Code intellisense is not showing type information for the store instance after passing it from useStore function, when used inside component. Works if imported in .ts file.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment