Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save joakimriedel/7aab32e9e69361241fc84e34d2eafab7 to your computer and use it in GitHub Desktop.
Save joakimriedel/7aab32e9e69361241fc84e34d2eafab7 to your computer and use it in GitHub Desktop.
Strictly typed mappers for vuex 4 and vue 3 using Typescript 4.4+

Vuex 4 is a great companion to Vue 3, but Vuex is falling behind on Typescript support. While waiting for better typing support in Vuex 5 or using Pinia, this wrapper might help.

Using this wrapper will correctly infer arguments and return types on your state, actions, getters and mutations.

It works both within your store(s) and any Vue components where you use the mapActions, mapState, mapGetters or mapMutations helpers from Vuex.

No more any will help you find many errors at compile-time!

<template>
<div>
{{bar}}
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import {
ModuleActions,
ModuleGetters,
} from "./Store/Modules/sampleStoreModule";
import {
mapGettersNamespaced,
mapActionsNamespacedWithRename,
} from "./Store/typedMappers";
export default defineComponent({
computed: {
...mapGettersNamespaced<ModuleGetters>()("sampleStoreModule", [
"bar",
]),
},
created: async function (): void {
await this.initializeModule("boo");
// note: this would be an error
// await this.initializeModule();
// note: as would this
// await this.initializeModule(123);
// note: this would return Promise even though defined as void in store!
// await this.resetFoo();
},
methods: {
...mapActionsNamespacedWithRename<ModuleActions>()("sampleStoreModule", {
initializeModule: "initializeAsync",
resetFoo: "noPayload"
}),
},
});
</script>
import { GetterTree, ActionTree, MutationTree, ActionContext } from "vuex";
import { RootState, RootGetters } from "..";
export type ModuleState = {
foo: string
};
/** exported state for injecting into store */
const state = (): ModuleState => ({
foo: "baz",
});
/** strict types for use with typed vuex mappers */
export type ModuleGetters = {
bar: string;
};
/** exported getters for injecting into store */
const getters = {
bar: (state) => {
return state.foo;
},
} as GetterTreeTyped<ModuleState, RootState, ModuleGetters, RootGetters>;
/** strict types for use with typed vuex mappers */
export type ModuleActions = {
initializeAsync: (payload: string) => Promise<void>;
mutateSomething: (payload: { a: string, b: string }) => void;
noPayload: () => void;
};
/** exported actions for injecting into store */
const actions = {
initializeAsync({ getters, dispatch }, payload) {
return dispatch("mutateSomething", { a: payload, b: getters.bar });
// note: this would give compiler error due to wrong spelling
// dispatch("mutateSometing");
// note: this would give compiler error due to missing payload
// dispatch("mutateSomething");
// note: this would give compiler error due to invalid payload
// dispatch("resetFoo", { a: payload, b: getters.bar });
},
mutateSomething({ commit }, payload) {
const {a, b} = payload;
commit("someMutation", a + b);
// note: this would give compiler error due to missing payload
// commit("someMutation");
// note: this would give compiler error due to invalid payload
// commit("someMutation", 42);
},
resetFoo({ commit }) {
commit("someMutation", "resetted");
},
} as ActionTreeTyped<ModuleState, RootState, ModuleActions, ModuleGetters, RootGetters, ModuleMutations>;
/** strict types for use with typed vuex mappers */
export type ModuleMutations = {
someMutation(payload: string): void;
};
/** exported mutations for injecting into store */
const mutations = {
someMutation(state, payload) {
state.foo = payload;
},
} as MutationTreeTyped<ModuleState, ModuleMutations>;
export default {
namespaced: true,
state,
getters,
actions,
mutations
};
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
CommitOptions,
DispatchOptions,
mapActions,
mapGetters,
mapMutations,
mapState,
} from "vuex";
import { Promisable, UnionToIntersection } from "type-fest";
type ActionMethod = (payload?: any) => Promisable<any>;
type ActionRecord = Record<string, ActionMethod>;
// credit
// https://stackoverflow.com/questions/69643202/how-to-use-typescript-generics-to-correctly-map-between-keys-and-values-with-ind
type GetKeyByValue<Obj, Value> = {
[Prop in keyof Obj]: Obj[Prop] extends Value ? Prop : never;
}[keyof Obj];
type ConditionalDispatchPayload<P> = undefined extends P
? [payload?: P, options?: DispatchOptions]
: [payload: P, options?: DispatchOptions];
type ConditionalDispatchArgs<F extends ActionMethod> = F extends (
injectee: ActionContextTyped,
payload: infer P
) => any
? ConditionalDispatchPayload<P>
: F extends (payload: infer P) => any
? ConditionalDispatchPayload<P>
: never;
type DispatchTyped<T, A extends ActionMethod> = (
type: T,
...args: ConditionalDispatchArgs<A>
) => Promise<Awaited<ReturnType<A>>>;
type MutationMethod = (payload?: any) => void;
type MutationRecord = Record<string, MutationMethod>;
type ConditionalMutationPayload<P> = undefined extends P
? [payload?: P, options?: CommitOptions]
: [payload: P, options?: CommitOptions];
type ConditionalMutationArgs<F extends MutationMethod> = F extends (
state: infer S,
payload: infer P
) => void
? ConditionalMutationPayload<P>
: F extends (payload: infer P) => void
? ConditionalMutationPayload<P>
: never;
type CommitTyped<T, M extends MutationMethod> = (
type: T,
...args: ConditionalMutationArgs<M>
) => ReturnType<M>;
type CommitRecord<M extends MutationRecord> = UnionToIntersection<
{
[K in keyof M]: CommitTyped<K, M[K]>;
}[keyof M]
>;
type GetterRecord = Record<string, any>;
type StateRecord = Record<string, any>;
type DispatchRecord<A extends ActionRecord> = UnionToIntersection<
{
[K in keyof A]: DispatchTyped<K, A[K]>;
}[keyof A]
>;
export type ActionContextTyped<
A extends ActionRecord = any,
S extends StateRecord = any,
RS extends StateRecord = any,
G extends GetterRecord = any,
RG extends GetterRecord = any,
M extends MutationRecord = any
> = {
dispatch: DispatchRecord<A>;
commit: CommitRecord<M>;
state: S;
getters: G;
rootState: RS;
rootGetters: RG;
};
export type ActionHandlerTyped<
F extends ActionMethod,
A extends ActionRecord,
S extends StateRecord,
RS extends StateRecord,
G extends GetterRecord,
RG extends GetterRecord,
M extends MutationRecord
> = F extends (...args: infer P) => infer R
? (injectee: ActionContextTyped<A, S, RS, G, RG, M>, ...args: P) => R
: never;
/** typed action tree */
export type ActionTreeTyped<
A extends ActionRecord = any,
S extends StateRecord = any,
RS extends StateRecord = any,
G extends GetterRecord = any,
RG extends GetterRecord = any,
M extends MutationRecord = any
> = {
[K in keyof A]: ActionHandlerTyped<A[K], A, S, RS, G, RG, M>;
};
type MutationHandlerTyped<F, S> = F extends (...args: infer P) => void
? (state: S, ...args: P) => void
: never;
export type MutationTreeTyped<
S extends StateRecord = any,
M extends MutationRecord = any
> = {
[K in keyof M]: MutationHandlerTyped<M[K], S>;
};
type GetterHandlerTyped<
F,
S extends StateRecord,
RS extends StateRecord,
G extends GetterRecord,
RG extends GetterRecord
> = (state: S, getters: G, rootState: RS, rootGetters: RG) => F;
export type GetterTreeTyped<
S extends StateRecord = any,
RS extends StateRecord = any,
G extends GetterRecord = any,
RG extends GetterRecord = any
> = {
[K in keyof G]: GetterHandlerTyped<G[K], S, RS, G, RG>;
};
// returns a type which skips the first context argument
type OmitActionContext<F> = F extends (...args: infer P) => infer R
? (...args: P) => Promise<Awaited<R>>
: never;
type ModuleState<Modules extends ModuleRecord> = {
[M in keyof Modules]: ReturnType<Modules[M]["state"]>;
};
type ModuleGettersTree<MT, MD extends ModuleDefinition> = UnionToIntersection<{
[GT in keyof MD["getters"] as AddNamespace<GT, MT>]: ReturnType<
MD["getters"][GT]
>;
}>;
type ModuleGetters<
Modules extends ModuleRecord,
Namespace = ""
> = UnionToIntersection<
{
[M in keyof Modules]: ModuleGettersTree<
AddNamespace<M, Namespace>,
Modules[M]
>;
}[keyof Modules]
>;
type ModuleDefinition<S extends StateRecord = any> = {
namespaced: boolean;
state: () => S;
getters: GetterTreeTyped;
actions: ActionTreeTyped;
mutations: any;
};
type ModuleRecord = Record<string, ModuleDefinition>;
type AddNamespace<Key, Namespace = ""> = Namespace extends string
? Key extends string
? `${Namespace}${"" extends Namespace ? "" : "/"}${Key}`
: never
: never;
type DispatchActions<
Actions extends ActionTreeTyped,
Namespace = ""
> = UnionToIntersection<
{
[K in keyof Actions]: DispatchTyped<AddNamespace<K, Namespace>, Actions[K]>;
}[keyof Actions]
>;
type DispatchModules<Modules extends ModuleRecord, Namespace = ""> = {
[K in keyof Modules]: DispatchActions<
Modules[K]["actions"],
AddNamespace<Modules[K] extends ModuleDefinition ? K : never, Namespace>
>;
};
type DispatchModulesTyped<Modules extends ModuleRecord> = UnionToIntersection<
DispatchModules<Modules>[keyof DispatchModules<Modules>]
>;
type CommitMutations<
Mutations extends MutationTreeTyped,
Namespace = ""
> = UnionToIntersection<
{
[K in keyof Mutations]: CommitTyped<
AddNamespace<K, Namespace>,
Mutations[K]
>;
}[keyof Mutations]
>;
/** Typed wrapper for mapActions on the root store
*
* NOTE: needs to be called with extra parenthesis to infer map keys correctly
*
* @example
* mapActionsRoot<TYPE>()(map)
*
*/
export const mapActionsRoot = <
S extends ActionRecord,
Key extends keyof S & string = keyof S & string
>() => {
function anonymous<Mp extends Key[]>(
map: Mp
): {
[K in Mp[number]]: OmitActionContext<S[K]>;
};
function anonymous<Mp extends Key[]>(map: Mp) {
return mapActions(map) as never;
}
return anonymous;
};
/** Typed wrapper for mapActions using a namespaced store
*
* NOTE: needs to be called with extra parenthesis to infer map keys correctly
*
* @example
* mapActionsNamespaced<TYPE>()(namespace, map)
*
*/
export const mapActionsNamespaced = <
S extends ActionRecord,
Key extends keyof S & string = keyof S & string
>() => {
function anonymous<Mp extends Key[]>(
namespace: string,
map: Mp
): {
[K in Mp[number]]: OmitActionContext<S[K]>;
};
function anonymous<Mp extends Key[]>(namespace: string, map: Mp) {
return mapActions(namespace, map) as never;
}
return anonymous;
};
/** Typed wrapper for mapActions using a namespaced store and renaming the keys
*
* NOTE: needs to be called with extra parenthesis to infer map keys correctly
*
* @example
* mapActionsNamespacedWithRename<TYPE>()(namespace, map)
*
*/
export const mapActionsNamespacedWithRename = <
S extends ActionRecord,
Key extends keyof S & string = keyof S & string
>() => {
function anonymous<Prop extends string, Mp extends Record<Prop, Key>>(
namespace: string,
map: Mp
): {
[P in Key as GetKeyByValue<Mp, P>]: OmitActionContext<S[P]>;
};
function anonymous<Prop extends string, Mp extends Record<Prop, Key>>(
namespace: string,
map: Mp
) {
return mapActions(namespace, map) as never;
}
return anonymous;
};
/** Typed wrapper for mapActions on the root store
*
* NOTE: needs to be called with extra parenthesis to infer map keys correctly
*
* @example
* mapActionsRoot<TYPE>()(map)
*
*/
export const mapGettersRoot = <
S extends GetterRecord,
Key extends keyof S & string = keyof S & string
>() => {
function anonymous<Mp extends Key[]>(
map: Mp
): {
[K in Mp[number]]: () => S[K];
};
function anonymous<Mp extends Key[]>(map: Mp) {
return mapGetters(map) as never;
}
return anonymous;
};
/** Typed wrapper for mapGetters using a namespaced store
*
* NOTE: needs to be called with extra parenthesis to infer map keys correctly
*
* @example
* mapGettersNamespaced<TYPE>()(namespace, map)
*
*/
export const mapGettersNamespaced = <
S extends GetterRecord,
Key extends keyof S & string = keyof S & string
>() => {
function anonymous<Mp extends Key[]>(
namespace: string,
map: Mp
): {
[K in Mp[number]]: () => S[K];
};
function anonymous<Mp extends Key[]>(namespace: string, map: Mp) {
return mapGetters(namespace, map) as never;
}
return anonymous;
};
/** Typed wrapper for mapGetters using a namespaced store and renaming the keys
*
* NOTE: needs to be called with extra parenthesis to infer map keys correctly
*
* @example
* mapGettersNamespacedWithRename<TYPE>()(namespace, map)
*
*/
export const mapGettersNamespacedWithRename = <
S extends GetterRecord,
Key extends keyof S & string = keyof S & string
>() => {
function anonymous<Prop extends string, Mp extends Record<Prop, Key>>(
namespace: string,
map: Mp
): {
[P in Key as GetKeyByValue<Mp, P>]: () => S[P];
};
function anonymous<Prop extends string, Mp extends Record<Prop, Key>>(
namespace: string,
map: Mp
) {
return mapGetters(namespace, map) as never;
}
return anonymous;
};
/** Typed wrapper for mapState on the root store
*
* NOTE: needs to be called with extra parenthesis to infer map keys correctly
*
* @example
* mapStateRoot<TYPE>()(map)
*
*/
export const mapStateRoot = <
S extends StateRecord,
Key extends keyof S & string = keyof S & string
>() => {
function anonymous<Mp extends Key[]>(
map: Mp
): {
[K in Mp[number]]: () => S[K];
};
function anonymous<Mp extends Key[]>(map: Mp) {
return mapState(map) as never;
}
return anonymous;
};
/** Typed wrapper for mapState using a namespaced store
*
* NOTE: needs to be called with extra parenthesis to infer map keys correctly
*
* @example
* mapStateNamespaced<TYPE>()(namespace, map)
*
*/
export const mapStateNamespaced = <
S extends StateRecord,
Key extends keyof S & string = keyof S & string
>() => {
function anonymous<Mp extends Key[]>(
namespace: string,
map: Mp
): {
[K in Mp[number]]: () => S[K];
};
function anonymous<Mp extends Key[]>(namespace: string, map: Mp) {
return mapState(namespace, map) as never;
}
return anonymous;
};
/** Typed wrapper for mapState using a namespaced store and renaming the keys
*
* NOTE: needs to be called with extra parenthesis to infer map keys correctly
*
* @example
* mapStateNamespacedWithRename<TYPE>()(namespace, map)
*
*/
export const mapStateNamespacedWithRename = <
S extends StateRecord,
Key extends keyof S & string = keyof S & string
>() => {
function anonymous<Prop extends string, Mp extends Record<Prop, Key>>(
namespace: string,
map: Mp
): {
[P in Key as GetKeyByValue<Mp, P>]: () => S[P];
};
function anonymous<Prop extends string, Mp extends Record<Prop, Key>>(
namespace: string,
map: Mp
) {
return mapState(namespace, map) as never;
}
return anonymous;
};
/** Typed wrapper for {@link external:mapMutations} on the root store
*
* NOTE: needs to be called with extra parenthesis to infer map keys correctly
*
* @example
* mapMutationsRoot<TYPE>()(map)
*
*/
export const mapMutationsRoot = <
S extends MutationRecord,
Key extends keyof S & string = keyof S & string
>() => {
function anonymous<Mp extends Key[]>(
map: Mp
): {
[K in Mp[number]]: S[K];
};
function anonymous<Mp extends Key[]>(map: Mp) {
return mapMutations(map) as never;
}
return anonymous;
};
/** Typed wrapper for mapMutations using a namespaced store
*
* NOTE: needs to be called with extra parenthesis to infer map keys correctly
*
* @example
* mapMutationsNamespaced<TYPE>()(namespace, map)
*
*/
export const mapMutationsNamespaced = <
S extends MutationRecord,
Key extends keyof S & string = keyof S & string
>() => {
function anonymous<Mp extends Key[]>(
namespace: string,
map: Mp
): {
[K in Mp[number]]: S[K];
};
function anonymous<Mp extends Key[]>(namespace: string, map: Mp) {
return mapMutations(namespace, map) as never;
}
return anonymous;
};
/** Typed wrapper for mapMutations using a namespaced store and renaming the keys
*
* NOTE: needs to be called with extra parenthesis to infer map keys correctly
*
* @example
* mapMutationsNamespacedWithRename<TYPE>()(namespace, map)
*
*/
export const mapMutationsNamespacedWithRename = <
S extends MutationRecord,
Key extends keyof S & string = keyof S & string
>() => {
function anonymous<Prop extends string, Mp extends Record<Prop, Key>>(
namespace: string,
map: Mp
): {
[P in Key as GetKeyByValue<Mp, P>]: S[P];
};
function anonymous<Prop extends string, Mp extends Record<Prop, Key>>(
namespace: string,
map: Mp
) {
return mapMutations(namespace, map) as never;
}
return anonymous;
};
export type StoreTyped<
RS extends StateRecord,
AT extends ActionTreeTyped,
MT extends MutationTreeTyped,
G extends GetterRecord,
Modules extends ModuleRecord
> = {
readonly state: RS & ModuleState<Modules>;
readonly getters: G & ModuleGetters<Modules>;
dispatch: DispatchActions<AT> & DispatchModulesTyped<Modules>;
commit: CommitMutations<MT>;
};
@d9k
Copy link

d9k commented Apr 26, 2024

That woulld be nice if mapStateRoot() could work with mapper callbacks too.

@d9k
Copy link

d9k commented Apr 26, 2024

something like

import { mapState } from 'vuex';
import { ComponentPublicInstance } from 'vue';

type CustomVue = ComponentPublicInstance & Record<string, any>;

type InlineComputed<T extends Function> = T extends (...args: any[]) => infer R ? () => R : never;

/** Based on MapperForState from node_modules/vuex/types/helpers.d.ts */
type ModMapperForState<S> = {
  <Map extends Record<string, (this: CustomVue, state: S, getters: any) => any> = {}>(map: Map): {
    [K in keyof Map]: InlineComputed<Map[K]>;
  };
};

export function constructMapStateRoot<S>() {
  return <M extends ModMapperForState<S>>(m: Parameters<M>[0]): ReturnType<M> => {
    return mapState(m as any) as ReturnType<M>;
  };
}
export mapStateRoot = constructMapStateRoot<StoreState>();
mapStateRoot({
  test: state => state.myAwesomeVariable
});

@d9k
Copy link

d9k commented Apr 26, 2024

In my code state type is correctly inferred but then this.test can't be used then. Can't understand the bug. Is it on TypeScript compiler side?

@d9k
Copy link

d9k commented Apr 26, 2024

import { mapState } from 'vuex';
import { ComponentPublicInstance } from 'vue';

type CustomVue = ComponentPublicInstance & Record<string, any>;

/** Based on MapperForState from node_modules/vuex/types/helpers.d.ts */
export function constructMapStateRoot<S>() {
  function anonymous<Map extends Record<string, (this: CustomVue, state: S, getters: any) => any>>(
    map: Map
  ): {
    [K in keyof Map]: () => ReturnType<Map[K]>;
  };
  function anonymous<Map extends Record<string, (this: CustomVue, state: S, getters: any) => any>>(map: Map) {
    return mapState(map as any) as never;
  }
  return anonymous;
}
export mapStateRoot = constructMapStateRoot<StoreState>();
mapStateRoot({
  test: state => state.myAwesomeVariable
});

This works. I don't understand why previous code doesn't

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