Cuando trabajamos con librer铆as de terceros a veces tenemos que lidiar con un tipado d茅bil. Es el caso de la librer铆a oficial de Vue para la administraci贸n del estado de la aplicaci贸n: Vuex

A pesar de ser una librer铆a muy utilizada en multitud de proyectos, la definici贸n de su API respecto a los tipos deja mucho que desear: poco soporte de tipos gen茅ricos y muchos tipos any por todo el c贸digo.

Esto conlleva a que cuando quieres utilizarla en tu desarrollo no tengas soporte del intellisense (autocompletado, definici贸n de propiedades y m茅todos, etc鈥) ni seguridad de tipos. En resumen, es como si tuvi茅ramos desactivado el soporte de TypeScript en ese c贸digo y fu茅ramos a ciegas.

Sample getters with errors

Pues bien, la buena noticia es que podemos "vitaminar" el tipado de Vuex mediante una serie de interfaces gen茅ricas y el uso del estilo de objeto que soporta las mutaciones y acciones de Vuex. En este art铆culo aportar茅 una soluci贸n sencilla que nos permitir谩 tener un punto de partida sobre c贸mo ayudarnos con TypeScript para completar las carencias de Vuex.

Conociendo el entorno

Como ejemplo de este art铆culo, trabajaremos sobre una aplicaci贸n t铆pica de carrito de la compra.

Tendremos que manejar mediante Vuex las siguientes caracter铆sticas:

  • Estado ra铆z de la aplicaci贸n: loading y snackbar.
  • Dos m贸dulos sin espacio de nombres: products y cart (aunque s贸lo veremos el c贸digo del primero).

Una de las primeras tareas que hacemos cuando desarrollamos con una librer铆a de terceros que expone tipos de TypeScript es, precisamente, ver los tipos que expone para ser usada. As铆 que vamos a vamos a ver los tipos principales que expone Vuex:

Se ha omitido c贸digo de la definici贸n para evitar ruido (路路路)

vuex/types/index.d.ts

export declare class Store<S> {
  constructor(options: StoreOptions<S>);

  readonly state: S;
  readonly getters: any;

  dispatch: Dispatch;
  commit: Commit;

  路路路
}

Como vemos, tenemos la declaraci贸n de una clase Store que admite un tipo gen茅rico llamado S.

Los tipos gen茅ricos en TypeScript nos permiten definir estructuras reutilizables aplicando un determinado tipo de dato en varios puntos de nuestro c贸digo.

En este caso, el gen茅rico S que le pasamos a Store ser谩 el que definir谩 la propiedad de s贸lo lectura state y que ser谩 el estado de nuestra aplicaci贸n. Por ahora, no tenemos que hacer nada especial, tan s贸lo definir una interface que contendr谩 nuestro estado ra铆z.

./src/store/root.models.ts

export interface RootState {
  loading: boolean;
  snackbar: Snackbar;
}

interface Snackbar {
  message: string;
  isActive: boolean;
  type?: "success" | "info" | "error";
}

export type SetSnackbar = Pick<Snackbar, "message" | "type">;

Para definir el tipo Snackbar hemos utilizado un tipo literal de string. De esta forma le decimos que la propiedad type s贸lo podr谩 contener como valores posibles "success", "info" o "error".

Y para el tipo a exportar SetSnackbar utilizamos el tipo de utilidad de TypeScript Pick<T,K>. Este tipo gen茅rico crea un nuevo tipo con las propiedades de T referidas en K. En nuestro caso, crearemos un tipo nuevo con las propiedades message y type de la interface Snackbar. Pod铆amos haber usado Omit pero lo veremos en otro caso m谩s adelante.

Ahora s贸lo nos falta crear un objeto que utilizaremos para setear el estado inicial de nuestro estado ra铆z.

./src/store/root.models.ts

路路路

export const initialRootState: RootState = {
  loading: false,
  snackbar: {
    message: "",
    isActive: false,
    type: undefined,
  },
};

Vamos a alimentar nuestro store con este estado ra铆z y utilizaremos la interface creada RootState para tipar el objeto store:

./src/store/index.ts

import { RootState, initialRootState } from "./root.models";
路路路

export const store = new Vuex.Store<RootState>({
  state: initialRootState,
});

Mutations

Lo primero que haremos ser谩 analizar los tipos que expone Vuex para las mutaciones.

vuex/types/index.d.ts

路路路

export interface MutationTree<S> {
  [key: string]: Mutation<S>;
}

export type Mutation<S> = (state: S, payload?: any) => any;

Para empezar, MutationTree es un tipo gen茅rico que permite pasarle la interface del estado (RootState en nuestro caso).

Cada propiedad en un objeto creado conmgom el tipo MutationTree tendr谩 como valor una funci贸n de tipo Mutation<S>. Esta 煤ltima interface viene a definirse como una funci贸n que admite dos par谩metros de entrada: state con el tipado del estado ra铆z (RootState) y un payload con un tipo any.

驴Qu茅 consecuencias nos trae manejar valores definidos con el tipo any?

Pues b谩sicamente le estamos diciendo a TypeScript que queremos deshabilitar la verificaci贸n de tipos y esto no es nada 贸ptimo para nosotros.

Normalmente el tipo any se utiliza cuando no conocemos el tipo de variables con el que vamos a trabajar, pero en nuestro caso, s铆 que sabemos qu茅 objetos vamos a definir como mutaciones, acciones, etc鈥 as铆 que no tiene mucho sentido mantener este tipado. En breve veremos c贸mo podemos mejorarlo.

Primero, vamos a hacer una introducci贸n al estilo de objeto que permite Vuex para acometer las mutaciones y despachar las acciones.

Estilo de objeto

store.commit("increment", { amount: 10 });

/** Son equivalentes */

store.commit({
  type: "increment",
  payload: { amount: 10 },
});

Esta caracter铆stica permite pasar un objeto que contenga una propiedad type con el nombre de la mutaci贸n o acci贸n y una propiedad payload con los par谩metros que queremos hacer llegar al m茅todo commit o dispatch del store, respectivamente.

Aprovechando esta caracter铆stica podemos crear una funci贸n que admita un payload y devolver un objeto con esa definici贸n (type y payload) para que el store lo entienda.

const increment = (payload: { amount: 10 }) => ({ type: "increment", payload: { amount: 10 } });

store.commit(increment({ amount: 10 }));

Sabiendo esto, mi propuesta se basa en utilizar esta caracter铆stica para crear un objeto que contenga estas funciones y utilizarlas donde sea necesario. De este modo tendremos la inferencia de tipos que no nos provee Vuex en nuestro store y componentes.

Object Style Mutation

Vamos a comenzar creando nuestros primeros tipos llamados helpers, y para ello, definiremos un fichero root.helpers.ts en el ra铆z de nuestro store:

DefineMutationTree

Este tipo gen茅rico vendr谩 a sustituir a MutationTree de Vuex. Lo que admite este tipo gen茅rico es la definici贸n de la interface de las mutaciones Mutation y del estado State.

Por cada propiedad definida en la interface que le pasaremos como mutaciones (gen茅rico Mutation) existir谩 una propiedad en este objeto cuyo valor ser谩 una funci贸n que admitir谩 el estado tipado y un handler (que har谩 referencia al objeto type y payload anterior). Este handler recibir谩 un objeto con una propiedad payload cuyo tipo ser谩 el que hemos definido previamente en nuestra interface Mutation.

./src/store/root.helpers.ts

export type DefineMutationTree<Mutation, State> = {
  [Prop in keyof Mutation]: (state: State, handler: { payload: Mutation[Prop] }) => void;
};

Quedar谩 m谩s claro cuando lo usemos. Primero, definimos la interface de nuestras mutaciones:

./src/store/root.mutations.ts

import { RootState } from "./root.models";

export interface RootMutations {
  setLoading: RootState["loading"];
  setSnackbar: RootState["snackbar"];
}

F铆jate que el tipo de dato que asignamos a cada propiedad ser谩 el tipo del payload que queremos pasar a las mutaciones.

Ahora, cuando vayamos a definir el objeto mutations con nuestra interface DefineMutationTree, al pasarle RootMutations y RootState nuestro IDE nos ir谩 indicando los valores que debemos rellenar sin posibilidad de equivocarnos, lo cual se traduce en menos errores, m谩s rapidez y m谩s control de tu c贸digo.

Mutations with types works

./src/store/root.mutations.ts

路路路
const mutations: DefineMutationTree<RootMutations, RootState> = {
  setLoading(state, { payload }) {
    state.loading = payload;
  },
  setSnackbar(state, { payload }) {
    state.snackbar = {
      message: payload.message,
      type: payload.type || "success",
      isActive: payload.isActive,
    };
  },
};

export default mutations;

DefineTypes

Al principio coment茅 que usar铆amos el estilo de objeto de Vuex para usarlo de forma segura, 驴verdad? Pues primero tenemos que definir la interface que deber谩n cumplir estos objetos.

En el fichero de root.helpers.ts crearemos el siguiente tipo gen茅rico llamado DefineTypes.

./src/store/root.helpers.ts

路路路
export type DefineTypes<Methods> = {
  [Prop in keyof Methods]: Methods[Prop] extends undefined
    ? () => { type: keyof Methods }
    : (payload: Methods[Prop]) => { type: keyof Methods; payload: Methods[Prop] };
};

Estamos definiendo un tipo gen茅rico que admite una interface llamada Methods (ya que nos servir谩 tanto para las mutaciones como para las acciones) la cual tendr谩 una propiedad existente en Methods y a trav茅s del tipo condicional T extends U ? X : Y, le estamos diciendo que si el tipo de dato es undefined asigne la definici贸n a la derecha del interrogante ? y en caso contrario, la definici贸n a la derecha de los dos puntos :. S铆, es un operador ternario de tipos y lo tenemos disponible en TypeScript desde la versi贸n 2.8.

La primera funci贸n no indica par谩metros de entrada y devolver谩 un objeto con una propiedad type con el nombre del m茅todo. La segunda funci贸n, admitir谩 un par谩metro de entrada payload con el tipo de dato indicado en la interface y devolver谩 un objeto con una propiedad type igual que la anterior y el payload recibido anteriormente.

Vamos a usarlo.

./src/store/root.mutations.ts

路路路
export const rootMutationsTypes: DefineTypes<RootMutations> = {
  setLoading: payload => ({ type: "setLoading", payload }),
  setSnackbar: payload => ({ type: "setSnackbar", payload }),
};

Root mutations types

A la hora de utilizarlo en conjunto con el resto de propiedades en el store haremos lo siguiente:

./src/store/index.ts

import mutations, { rootMutationsTypes, RootMutations } from "./root.mutations";
路路路
export const store = new Vuex.Store<RootState>({
  strict: true,
  state: initialRootState,
  mutations, // <- Agregamos el objeto con las mutaciones
});

// Exportamos nuestro objeto ayudante para usarlo en los componentes (contiene nuestras funciones con el estilo de objeto)
export const rootTypes = {
  mutations: rootMutationsTypes,
};

Y para usarlo importaremos este objeto rootTypes y lo pasaremos como par谩metro de entrada al m茅todo commit del store (o this.$store si estamos en los componentes, por ejemplo):

const actions = {
  getAllProducts: ({ commit }) => {
    commit(rootMutationsTypes.setLoading(true));
  },
};

Veamos c贸mo se comporta:

Mutations types works

Actions

Para las acciones haremos exactamente lo mismo que con las mutaciones.

Primero, vamos a ver qu茅 tipos nos ofrece Vuex al respecto.

vuex/types/index.d.ts

路路路
export interface ActionTree<S, R> {
  [key: string]: Acocltion<S, R>;
}

export type Action<S, R> = ActionHandler<S, R> | ActionObject<S, R>;

export type ActionHandler<S, R> = (this: Store<R>, injectee: ActionContext<S, R>, payload?: any) => any;
export interface ActionObject<S, R> {
  root?: boolean;
  handler: ActionHandler<S, R>;
}

Al igual que con MutationTree tenemos una tipado muy d茅bil con any, tanto en el payload como en el retorno de las acciones. As铆 que vamos a ver c贸mo solucionarlo.

Esta vez comenzaremos definiendo la interface que deber谩 cumplir nuestras acciones. Recuerda, al igual que con las mutaciones, vamos a definir el nombre de nuestra acci贸n como clave de la propiedad y como tipo de dato el par谩metro de entrada de la acci贸n.

./src/store/root.actions.ts

import { RootState, SetSnackbar } from "./root.models";

export interface RootActions {
  showSnackbar: SetSnackbar;
}

DefineActionTree

Ahora crearemos un nuevo tipo llamado DefineActionTree en nuestro fichero de helpers.

./src/store/root.helpers.ts

import { Store, ActionContext } from "vuex";
路路路
export type DefineActionTree<Action, State, RootState> = {
  [Prop in keyof Action]: Action[Prop] extends undefined
    ? (
        this: Store<RootState>,
        ctx: ActionContext<State, RootState>,
      ) => void | Promise<any>
    : (
        this: Store<RootState>,
        ctx: ActionContext<State, RootState>,
        handler: { payload: Action[Prop] },
      ) => void | Promise<any>;
};

Muy parecido a la definici贸n de DefineMutationTree, la diferencia es que las acciones reciben el contexto del store y para ello hacemos uso de ActionContext de Vuex.

Vamos a usarlo todo en conjunto en nuestro fichero de acciones:

./src/store/root.actions.ts

import { RootState, SetSnackbar } from "./root.models";
import { DefineActionTree, DefineTypes } from "./store.helpers";
import { rootMutationsTypes } from "./root.mutations";

export interface RootActions {
  showSnackbar: SetSnackbar;
}

const actions: DefineActionTree<RootActions, RootState> = {
  showSnackbar({ commit }, { payload }) {
    commit(rootMutationsTypes.setSnackbar({ ...payload, isActive: true }));

    setTimeout(() => {
      commit(rootMutationsTypes.setSnackbar({ ...payload, isActive: false }));
    }, 3000);
  },
};

export const rootActionsTypes: DefineTypes<RootActions> = {
  showSnackbar: payload => ({ type: "showSnackbar", payload }),
};

export default actions;

Define Actions Tree

F铆jate que estamos importando el objeto rootMutationsTypes para hacer uso de las mutaciones previamente definidas. Todo este c贸digo te aporta seguridad de tipos para trabajar m谩s c贸modamente, y lo mejor es que cuando lo usamos en nuestros componentes tambi茅n tenemos seguridad de tipos, cosa que antes no ten铆amos.

Vamos a alimentar nuestro store con los objetos creados:

./src/store/index.ts

路路路
import actions, { rootActionsTypes, RootActions } from "./root.actions";

export const store = new Vuex.Store<RootState>({
  strict: true,
  state: initialRootState,
  mutations,
  actions,
});

export const rootTypes = {
  actions: rootActionsTypes,
  mutations: rootMutationsTypes,
};

Define Actions works

Getters

Si recordamos c贸mo estaba definida la propiedad de s贸lo lectura getters en la clase Store de Vuex鈥

./vuex/types/index.d.ts

export declare class Store<S> {
  constructor(options: StoreOptions<S>);

  readonly state: S;
  readonly getters: any;

  dispatch: Dispatch;
  commit: Commit;

  路路路
}

Vemos que se nos presenta un gran problema respecto a lo que el tipado se refiere.

La propiedad getters est谩 definida con el tipo any de TypeScript, lo que significa que getters podr谩 ser cualquier cosa y por tanto, nuestro IDE no podr谩 trabajar adecuadamente.

Cuando vayamos a utilizar esta propiedad en nuestros componentes estaremos totalmente a ciegas con los problemas que conlleva. Pero es que adem谩s en el propio uso en el store tendremos los mismos problemas鈥 observa la definici贸n:

./vuex/types/index.d.ts

export interface GetterTree<S, R> {
  [key: string]: Getter<S, R>;
}

export type Getter<S, R> = (state: S, getters: any, rootState: R, rootGetters: any) => any;

Seguimos teniendo tipos any por todos lados鈥

Vamos a ver c贸mo podr铆amos implementar una soluci贸n r谩pida para subsanarlo. Trabajaremos en nuestro fichero store.helpers.ts

./src/store/store.helpers.ts

路路路

export type DefineGetterTree<Getter, State, RootState = {}, RootGetter = {}> = {
  [K in keyof Getter]: (
    state: State,
    getters: Getter,
    rootState: RootState,
    rootGetters: RootGetter,
  ) => Getter[K];
};

export type GetterHelper<Getter> = { [Prop in keyof Getter]: Getter[Prop] };

export type StoreTS<State, Getters> = Omit<Store<State>, "getters"> & {
  readonly getters: GetterHelper<Getters>;
};

Vamos por partes. Primero, DefineGetterTree.

DefineGetterTree

export type DefineGetterTree<Getter, State, RootState = {}, RootGetter = {}> = {
  [Prop in keyof Getter]: (
    state: State,
    getters: Getter,
    rootState: RootState,
    rootGetters: RootGetter
  ) => Getter[Prop];
};

Hemos creado un tipo gen茅rico que admitir谩 la definici贸n de los getters Getter, el estado local State, el estado ra铆z RootState (si estuvi茅ramos en un m贸dulo) y los getters del ra铆z RootGetter (si estuvi茅ramos tambi茅n en un m贸dulo).

Antes de ver c贸mo podemos usarlo, vamos a crear la definici贸n de nuestros Getters:

./src/store/root.getters.ts

import { RootState } from "./root.models";

export interface RootGetters {
  snackbar: RootState["snackbar"];
}

export default getters;

En esta definici贸n lo que hacemos es decir qu茅 propiedades tendr谩 nuestros getters (en este caso, una propiedad snackbar) y que la misma tendr谩 un tipo RootState["snackbar"]. Estamos aprovechando la funcionalidad de TypeScript de acceder a los tipos de una interface mediante su 铆ndice.

Si recordamos el funcionamiento de Vuex, un getter no es m谩s que una funci贸n que retorna el estado manipulado, es decir, como una computed property. Gracias a esta interface que hemos definido, lo que estamos indicando es el retorno que tendr谩 esa funci贸n getter llamada snackbar.

Ahora podemos ver el uso de nuestra interface gen茅rica DefineGetterTree.

Vamos a crear el objeto que expondremos como getters para nuestro store:

./src/store/root.getters.ts

import { DefineGetterTree } from "./store.helpers";
import { RootState } from "./root.models";

export interface RootGetters {
  snackbar: RootState["snackbar"];
}

const getters: DefineGetterTree<RootGetters, RootState> = {
  snackbar: state => state.snackbar,
};

export default getters;

F铆jate que cuando creamos DefineGetterTree, dijimos que por cada propiedad en RootGetters ([Prop in keyof Getter]) crear铆amos una propiedad en este nuevo objeto que tendr铆a como valor una funci贸n que recibir铆a como par谩metros de entrada, entre otras cosas, el estado State; y que tendr铆a como retorno de dicha funci贸n el tipo que le dijimos en nuestra interface (Getter[Prop]):

export type DefineGetterTree<Getter, State, RootState = {}, RootGetter = {}> = {
  [Prop in keyof Getter]: (
    state: State,
    getters: Getter,
    rootState: RootState,
    rootGetters: RootGetter
  ) => Getter[Prop];
};

Bien, por ahora hemos definido c贸mo ser谩 nuestros getters pero no hemos dicho c贸mo le decimos a Vuex que lo use.

Vamos a alimentar nuestro store con este objeto y veremos c贸mo podemos salvar el any que vimos al principio.

StoreTS

./src/store/store.helpers.ts

路路路

export type GetterHelper<Getter> = { [Prop in keyof Getter]: Getter[Prop] };

export type StoreTS<State, Getters> = Omit<Store<State>, "getters"> & {
  readonly getters: GetterHelper<Getters>;
};

Aqu铆 estamos usando algunos tipos avanzados de TypeScript para ayudarnos a conseguir nuestro objetivo. Vamos por partes:

Primero, StoreTS es una interface gen茅rica que admite la interface del estado State y la interface de los getters Getters. Esta interface gen茅rica hace una uni贸n de tipos un poco especial:

  • Por una parte, tenemos un tipo de utilidad Omit<T,K> que construye un tipo tomando todas las propiedades de T excepto K. En nuestro caso, todas las propiedades de la interface Store<State> excepto la propiedad getters (recuerda, Store viene de los tipos de Vuex y esta propiedad getters estaba con tipo any, por eso no nos interesa).
  • Por otra parte, y mediante la uni贸n & le decimos que agregue una propiedad de s贸lo lectura llamada getters y para la cual su tipo ser谩 GetterHelper<Getters>.

As铆 que el resultado ser谩 una sobreescritura de tipos donde la instancia de nuestro store ser谩 el tipado que viene por defecto en Vuex excepto para los getters, que ahora tendr谩n inferencia de tipos gracias a esta definici贸n de tipos.

驴Y c贸mo lo usamos? Veamos:

./src/store/index.ts

路路路
import getters, { RootGetters } from "./root.getters";

Vue.use(Vuex);

export const store = new Vuex.Store<RootState>({
  strict: true,
  state: initialRootState,
  mutations,
  actions,
  getters, // <- alimentamos con nuestro objeto getters, por tanto this.$store.getters no tendr谩 inferencia de tipos (funcionamiento normal)
});

路路路
// Pero exportaremos este objeto que s铆 tendr谩 la inferencia de tipos para getters, p.ej: store.getters.snackbar
export default store as StoreTS<RootState, RootGetters & CartGetters>;

De esta forma, cuando queramos usar los getters tipados en nuestros componentes tan s贸lo deberemos importar este objeto cuyo tipos estar谩n ampliados.

./src/store/index.ts

<script lang="ts">
import Vue from "vue";
import store from "../store";

export default Vue.extend({
  name: "Snackbar",
  computed: {
    snackbar() {
      return store.getters.snackbar;
    },
  },
});
</script>

Y ahora con los nuevos tipos, vamos a verlo en funcionamiento:

Typings getters

F铆jate que ya no es necesario indicar el tipo de retorno de las computed property porque es capaz de inferir el tipo

M贸dulos

Por ahora hemos cubierto c贸mo trabajar con la librer铆a Vuex y sus tipos en el ra铆z, pero normalmente usaremos la caracter铆stica de m贸dulos para escalar nuestro estado.

Products State

Vamos a comenzar por definir el estado del m贸dulo "Products" y el objeto que usaremos para inicializarlo:

./src/store/modules/products/products.models.ts

export interface ProductsState {
  all: Product[];
}

export interface Product {
  id: number;
  title: string;
  price: number;
  inventory: number;
}

export const initialProductsState: ProductsState = {
  all: [],
};

Adem谩s vamos a necesitar extender la interface del estado ra铆z para darle cabida al estado de este m贸dulo mediante la propiedad products que tendr谩 el estado ra铆z.

./src/store/modules/products/products.models.ts

路路路
export type ExtendedProductsState = { products?: ProductsState };

Y ahora debemos utilizarlo en el estado ra铆z para ampliarlo.

./src/store/root.models.ts

路路路
import { ExtendedProductsState } from "./modules/products";

/** Root State */
export interface RootState extends ExtendedProductsState {
  loading: boolean;
  snackbar: Snackbar;
}
路路路

Products Mutations

Por simplicidad, vamos a mostrar el fichero completo de products.mutations.ts

./src/store/modules/products/products.mutations.ts

import { DefineMutationTree, DefineTypes } from "../../store.helpers";
import { RootState } from "../../root.models";
import { ProductsState, Product } from "./products.models";

export interface ProductsMutations {
  setProducts: Product[];
  decrementProductInventory: Product["id"];
}

const mutations: DefineMutationTree<ProductsMutations, ProductsState> = {
  setProducts: (state, { payload }) => {
    state.all = payload;
  },

  decrementProductInventory: (state, { payload }) => {
    state.all.find(p => p.id === payload)!.inventory--;
  },
};

export const productsMutationsTypes: DefineTypes<ProductsMutations> = {
  setProducts: payload => ({ type: "setProducts", payload }),
  decrementProductInventory: payload => ({
    type: "decrementProductInventory",
    payload,
  }),
};

export default mutations;

Products Actions

Por simplicidad, vamos a mostrar el fichero completo de products.actions.ts

./src/store/modules/products/products.actions.ts

路路路
import { rootMutationsTypes } from "../../root.mutations";

export interface ProductsActions {
  getAllProducts: undefined;
}

const actions: DefineActionTree<ProductsActions, ProductsState> = {
  getAllProducts: ({ commit }) => {
    commit(rootMutationsTypes.setLoading(true));
    路路路
  },
};

export const productsActionsTypes: DefineTypes<ProductsActions> = {
  getAllProducts: () => ({ type: "getAllProducts" }),
};

export default actions;

F铆jate c贸mo importando los helpers del ra铆z podemos hacer uso de las funciones tipadas

Products Getters

Por simplicidad, vamos a mostrar el fichero completo de products.getters.ts

./src/store/modules/products/products.getters.ts

import { DefineGetterTree } from "../../store.helpers";
import { RootState } from "../../root.models";
import { ProductsState, Product } from "./products.models";

export interface ProductsGetters {
  allProducts: Product[];
}

const getters: DefineGetterTree<ProductsGetters, ProductsState, RootState> = {
  allProducts: state => state.all,
};

export default getters;

Una vez creado el fichero de mutaciones vamos a a帽adirlo a nuestro store.

路路路
import { products, productsTypes } from "./modules/products";

export const store = new Vuex.Store<RootState>({
  strict: true,
  state: initialRootState,
  mutations,
  actions,
  getters,
  modules: {  // Agregamos los m贸dulos al store
    products,
  },
});

export const rootTypes: HelperTypes<RootMutations, RootActions> = {
  actions: rootActionsTypes,
  mutations: rootMutationsTypes,
};

/** Helper types Object */
export const storeTypes = {
  root: rootTypes,
  products: productsTypes,
};

Yo por comodidad he creado un objeto que he llamado storeTypes donde voy agregando, bajo el nombre de los m贸dulos o del ra铆z, los objetos correspondientes con las acciones y mutaciones de todo el store.

As铆 cuando vaya a utilizarlo s贸lo debes indicar la ruta al m茅todo:

<script lang="ts">
import Vue from "vue";
import store, { storeTypes } from "../store";
import { Product } from "../store/modules/products";

export default Vue.extend({
  name: "ProductList",
  computed: {
    products() {
      return store.state.products!.all;
    },
  },
  methods: {
    addToCart(product: Product) {
      store.dispatch(storeTypes.cart.actions!.addToCart(product));
    },
  },
});
</script>

Store with types

Y hasta aqu铆 el post sobre c贸mo aumentar tu productividad usando Vuex con TypeScript.

Este post nace de la charla que impart铆 en el JSDay Canarias 2019. El c贸digo completo lo puedes encontrar en este enlace.

Adem谩s, de esta charla surgi贸 la idea de crear un paquete de npm con los tipos m谩s completos ya disponibles para trabajar, as铆 que si te ha parecido interesante el tema y quieres usarlos, los tienes a tu disposici贸n en @lissette.ibnz/vuex-extended-types

Ya sabes npm i -D @lissette.ibnz/vuex-extended-types

Espero que te haya resultado 煤til el art铆culo, y cualquier duda/pregunta/sugerencia pod茅is encontrarme en twitter como @LissetteIbnz

Nos vemos 馃枛馃槃