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 🖖😄