Skip to content

Instantly share code, notes, and snippets.

@soerenmartius
Last active June 29, 2023 02:28
Show Gist options
  • Save soerenmartius/ad62ad59b991c99983a4e495bf6acb04 to your computer and use it in GitHub Desktop.
Save soerenmartius/ad62ad59b991c99983a4e495bf6acb04 to your computer and use it in GitHub Desktop.
Vue 3 with Typescriptt and Vuex 4 Typed Modules Examples ( with real types )
import {
ActionContext,
ActionTree,
GetterTree,
MutationTree,
Module,
Store as VuexStore,
CommitOptions,
DispatchOptions,
} from 'vuex'
import { State as RootState } from '@/store'
// Declare state
export type State = {
isAuthenticated: boolean
}
// Create initial state
const state: State = {
isAuthenticated: false,
}
// mutations enums
export enum MutationTypes {
SET_USER_AUTHENTICATED = 'SET_USER_AUTHENTICATED',
}
// Mutation contracts
export type Mutations<S = State> = {
[MutationTypes.SET_USER_AUTHENTICATED](state: S): void
}
// Define mutations
const mutations: MutationTree<State> & Mutations = {
[MutationTypes.SET_USER_AUTHENTICATED](state: State) {
state.isAuthenticated = true
},
}
// Action enums
export enum ActionTypes {
SIGNIN = 'SIGNIN',
}
// 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.SIGNIN](
{ commit }: AugmentedActionContext,
payload: { username: string; password: string },
): void
}
// Define actions
export const actions: ActionTree<State, RootState> & Actions = {
async [ActionTypes.SIGNIN](
{ commit },
payload: { username: string; password: string },
) {
try {
// some logic that logs a user in
} catch (err) {
// some error handling logic
}
},
}
// getters types
export type Getters = {
isAuthenticated(state: State): boolean
}
// getters
export const getters: GetterTree<State, RootState> & Getters = {
isAuthenticated: (state) => {
return state.isAuthenticated
},
}
//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 AuthModule: Module<State, RootState> = {
state,
mutations,
actions,
getters,
// Namespacing Vuex modules is tricky and hard to type check with typescript.
// Instead of namespacing, we could create our own namespacing mechanism by
// prefixing the value of the TypeScript enum with the namespace, e.g.
// enum TodoActions {
// AddTodo = 'TODO__ADD_TODO'
// }
// namespaced: true,
}
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) {
// As this is just an example, a Service Implementation is out of scope.
// A service in my case is basically an wrapper for a certain API.
const domainService = new DomainService()
const domains = await domainService.getDomains(teamId)
commit(MutationTypes.SET_DOMAINS, domains)
},
async [ActionTypes.FETCH_REDIRECTIONS](
{ commit },
payload: { teamId: string; domainName: string },
) {
const redirectionService = new RedirectionService()
const { teamId, domainName } = payload
let redirections: Redirection[] = []
redirections = redirections.concat(
await redirectionService.getRedirections(teamId, domainName),
)
commit(MutationTypes.SET_REDIRECTIONS, redirections)
},
}
// 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,
}
import { createStore, createLogger } from 'vuex'
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:
process.env.NODE_ENV === 'production'
? []
: [createLogger()],
modules: { AuthModule, DomainModule },
})
export function useStore(): Store {
return store as Store
}
export default store
@WangHansen
Copy link

Thanks! But it seems not possible to add some local state to the root state. Do you know how to achieve that?

@soerenmartius
Copy link
Author

Thanks! But it seems not possible to add some local state to the root state. Do you know how to achieve that?

yes sure, i can provide a new refactored example in a couple of days.

@stevenfowler16
Copy link

Just wanted to say thanks for this. There were a few longer tutorials that seemed to be quite convoluted but this got me right where I wanted

@balck-paint
Copy link

Can it be simplified

@tance77
Copy link

tance77 commented Jun 29, 2021

My getters seem to be throwing an exception and I'm not really sure why. Any suggestions?

EDIT

This seems to be an issue with vuex-multi-tab-state

@xkubow
Copy link

xkubow commented Aug 16, 2021

Will be there some option for named modules? I can't make the state accessible thru namespaced module.

@soerenmartius
Copy link
Author

I haven't implemented namespacing yet and meanwhile stopped working with Vuex as it's not really fully typed itself. It might be a good idea to wait for Vuex 5 or even question if you need Vuex in the first place.

@Longwater1234
Copy link

this looks extremely complicated. 🙄

@nelisbijl
Copy link

Good to see that even the author has abandoned his 'solution'
Where Vue 3 is all about Typescript, Vuex 4 definitely missed that. Their (almost) only argument for using Vuex is it's great logging. That's mainly needed because of the poor implementation of their store depending on name string references.
Not sure what Vuex 5 will bring but they have to hurry or everyone will have found/created an alternative.

@ux-engineer
Copy link

Just recently Evan You confirmed in Twitter that Pinia is Vuex 5.

@nelisbijl
Copy link

Okay, that was a useful tip

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