Guide on accessing intellisense for mapGetters, mapActions in Vuex using TypeScript without the need for class-style or decorators syntax

I have been using Vue.js and Vuex for a while now, but always with JavaScript.

Recently, I decided to try using Vue with TypeScript, specifically with nuxt.js, but without utilizing decorators or style-class-components. I want to stick to the normal Vue syntax.

Below is the code from my Vuex store:

/store/todos/types.ts

export interface Todo {
  id: number
  text: string
  done: boolean
}

export interface TodoState {
  list: Todo[]
}

/store/todos/state.ts

import { TodoState } from './types'

export default (): TodoState => ({
  list: [
    {
      id: 1,
      text: 'first todo',
      done: true
    },
    {
      id: 2,
      text: 'second todo',
      done: false
    }
  ]
})

/store/todos/mutations.ts

import { MutationTree } from 'vuex'
import { TodoState, Todo } from './types'

export default {
  remove(state, { id }: Todo) {
    const index = state.list.findIndex((x) => x.id === id)
    state.list.splice(index, 1)
  }
} as MutationTree<TodoState>

/store/todos/actions.ts

import { ActionTree } from 'vuex'
import { RootState } from '../types'
import { TodoState, Todo } from './types'

export default {
  delete({ commit }, { id }: Todo): void {
    commit('remove', id)
  }
} as ActionTree<TodoState, RootState>

/store/todos/getters.ts

import { GetterTree } from 'vuex'
import { RootState } from '../types'
import { TodoState, Todo } from './types'

export default {
  list(state): Todo[] {
    return state.list
  }
} as GetterTree<TodoState, RootState>

Here is the code in my component:

<template>
  <div>
    <ul>
      <li v-for="todo in todos" :key="todo.id">
        {{ todo.text }}
        <button @click="destroy(todo)">delete</button>
      </li>
    </ul>
  </div>
</template>

<script lang="ts">
import Vue from 'vue'
import { mapGetters, mapActions } from 'vuex'

export default Vue.extend({
  computed: {
    ...mapGetters({
      todos: 'todos/list'
    })
  },
  methods: {
    ...mapActions({
      destroy: 'todos/delete'
    })
  }
})
</script>

Everything seems to be working fine, except for the lack of auto-complete/intellisense for getters or actions provided by Vuex.

If anyone can help me with this issue, I would greatly appreciate it!

Thank you so much! o/

Answer №1

Vuex, as it stands now, may not integrate smoothly with Typescript. However, it is expected that this will change in Vue 3.

Like many others, I prefer not to use @Component decorators, especially since they have been deprecated. When working with the default Vue typescript component style:

<script lang="ts">
  import Vue from 'vue';
  export default Vue.extend({...})
</script>

...after exploring various options, I discovered that the simplest solution is actually a plugin that utilizes decorators: vuex-module-decorators

My approach with Vuex modules involves keeping the parent state clean and utilizing namespaced modules. This helps in cases where creating additional modules later on is deemed necessary for a cleaner structure.

Here's what the store setup looks like:

import Vue from 'vue';
import Vuex from 'vuex';
import { getModule } from 'vuex-module-decorators';
import Whatever from '@/store/whatever';

Vue.use(Vuex);

const store = new Vuex.Store({
  modules: {
    whatever: Whatever
  }
});

getModule(Whatever, store); // essential for proper typesscript functionality

export type State = typeof store.state;
export default store;

Below are examples of using mapState, mapGetters, or computed properties directly linked to the store:

computed: {
  ...mapGetters({
    foo: 'whatever/foo',
    bar: 'whatever/bar'
  }),
  ...mapState({
    prop1: (state: State): prop1Type[] => state.whatever.prop1,
    prop2: (state: State): number | null => state.whatever.prop2
  }),
  baz: {
    get: function(): number {
      return this.$store.state.whatever.baz;
    },
    set: function(value: number) {
      if (value !== this.baz) {
        this.$store.dispatch('whatever/setBaz', value);
      }
    }
  }
}

Using baz as a v-model is now possible. It's important to note that mapGetters should reference actual module store getters.

[....]

Answer №2

As I was exploring a resolution to this particular issue, I came across this query. After conducting some tests, I believe I have found a solution.

The key lies in encapsulating the methods mapGetters and mapActions so that Typescript can deduce the types involved accurately. This approach not only flags compile-time errors for incorrect keys provided to the mapper but also ensures accurate return types without any instances of any.

// defines a type that omits the initial context argument
type OmitActionContext<F> = F extends (
  injectee: ActionContext<any, any>,
  payload: infer P
) => infer R
  ? (payload?: P) => Promise<PromiseValue<R>>
  : never;

// structure of action methods
type ActionMethod = (
  injectee: ActionContext<any, any>,
  ...args: any[]
) => Promisable<any>;

/** Typed wrapper for mapActions using a namespaced store and renaming the keys
 *
 *  NOTE: additional parentheses are required for proper inference of map keys
 *
 * @example
 *  mapActionsNamespacedWithRename<TYPE>()(namespace, map)
 *
 */
export const mapActionsNamespacedWithRename = <
  S extends Record<keyof S & string, ActionMethod>,
  Keys extends keyof S & string = keyof S & string
>() => {
  function anonymous<Prop extends string, Mp extends Record<Prop, Keys>>(
    namespace: string,
    map: Mp
  ): {
    [P in Keys as GetKeyByValue<Mp, P>]: OmitActionContext<S[P]>;
  };
  function anonymous<Prop extends string, Mp extends Record<Prop, Keys>>(
    namespace: string,
    map: Mp
  ) {
    return mapActions(namespace, map);
  }
  return anonymous;
};

By using the above wrapper, the correct inference of payload and Promise return types is ensured.

To properly define your /store/todos/actions.ts, the following typings need to be added:

import { ActionContext } from 'vuex'
import { RootState, RootGetters } from '../types'
import { TodoState, Todo } from './types'

export type TodoActionMethods = {
  delete: (injectee: ActionContext<TodoState, RootState>, payload: Todo) => void
}

export default {
  delete({ commit }, payload): void {
    const {id} = payload;
    commit('remove', id)
  }
} as ActionTreeTyped<
  TodoState,
  RootState,
  TodoActionMethods,
  TodoGetters,
  RootGetters
>

Subsequently, utilize the aforementioned wrapper in your component. Note the inclusion of extra parenthesis and generic type.

  methods: {
    ...mapActionsNamespacedWithRename<TodoActionMethods>()("todos", {
      destroy: 'delete'
    })
  }

This approach eliminates the need for module augmentation and relies solely on the magic of Typescript!

Explore the complete solution in the linked gist

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Unveiling the magic behind using jasmine to spy on a generic

I am trying to spy on a generic method in TypeScript, but Jasmine is not recognizing it. Here is the code snippet: http: HttpClient <- Not actual code, just showing type. ... this.http.get<Customer[]>(url); In this code, I am trying to mock the ...

How to Perform a Redirect to a Named Route in Vue.js?

Currently, I am in the process of working on a Vue.js project and have set up a route with the following configuration: { path: '/Cart', component: Cart, props: (route) => ({ payment_id: route.query.payment_id, payment_request_id: ...

Unable to utilize combined data types in React properties

In my theme.interface.ts file, I defined the following 2 types: type ThemeSize = | 'px' | '1' | '1/2' | 'full' | 'fit' type ThemeWidthSpecific = 'svw' | 'lvw' | 'dvw&apos ...

Code coverage analysis in a node.js TypeScript project consistently shows no coverage metrics

I'm currently working on a backend TypeScript project where I'm aiming to obtain coverage reports for unit test cases. However, Jest is returning empty coverage reports both in the terminal and in the HTML report, with no information provided. Ev ...

Framer Motion's AnimatePresence feature fails to trigger animations when switching between pages

I'm running into issues with the Framer Motion library, specifically with the AnimatePresence transition. I've managed to make it work in normal situations, but when I encapsulate my Routes within a custom implementation, the exit animation fails ...

Angular: Real-time monitoring of changes in the attribute value of a modal dialog and applying or removing a class to the element

I cannot seem to figure out a solution for the following issue: I have two sibling div elements. The second one contains a button that triggers a modal dialog with a dark overlay. However, in my case, the first div appears on top of the modal dialog due to ...

Transfer information from an HTML document to a Vue application that has been registered

I have a Vue application set up in the following manner: import { createApp } from 'vue'; import RecommendedJobsWidget from './RecommendedJobsWidget.vue' createApp(RecommendedJobsWidget).mount("#recommendedJobsWidgetInstance" ...

Encountering issues while attempting to transmit several files to backend in React/NestJS resulting in a BAD REQUEST error

My goal is to allow users to upload both their CV and image at the same time as a feature. However, every time I attempt to send both files simultaneously to the backend, I encounter a Bad Request error 400. I have made various attempts to troubleshoot th ...

Error: Certain Prisma model mappings are not being generated

In my schema.prisma file, I have noticed that some models are not generating their @@map for use in the client. model ContentFilter { id Int @id @default(autoincrement()) blurriness Float? @default(0.3) adult ...

I am experiencing issues with the functionality of the navigation drawer on my page (using Vuetify)

One challenge I'm facing is with a vuetify navigation drawer within the navbar of my vuejs app. While it opens and closes properly, none of the items inside are clickable as they should be acting as links to other pages. Currently, only the logout but ...

Is there a way for React to recognize index.ts as the root file of a folder?

I recently started working on a new React project and I'm facing an issue with resolving the index.js file as the folder being imported in another component. Expected outcome: No errors // src/pages/router.tsx import HomePage from './home-page` ...

Create a functioning implementation for retrieving a list of objects from a REST API

I am looking to incorporate an Angular example that retrieves a list from a REST API. Here is what I have attempted: SQL query: @Override public Iterable<Merchants> findAll() { String hql = "select e from " + Merchants.class.getName ...

Can an event be passed from one Vue.js component to another without relying on a global EventBus?

I have two independent components that are not directly related to each other. However, I want them to be able to communicate so that when an event is triggered in one component, the other component can respond accordingly. For example, let's conside ...

Encountered a CSS error while trying to compile the project with npm build

When attempting to build the project, I encountered a postcss error. After some debugging, I discovered that the imports below were causing the issue: @import "@material/button/dist/mdc.button.min.css"; /*material text box css*/ @import "@material/float ...

Creating TypeScript types for enums can make your code more robust and ensure that you are using

I need to create an interface based on the values of an enum for a React use-case. The enum contains key value pairs that are used as form IDs. When the value of an input element is changed in an event listener, I want to set the state using this.setState( ...

How to add sass-loader to a Vue 3 project

I attempted to integrate a sass/scss loader into my project created with vue CLI. After running the following script: $ npm install -D sass-loader@^10 sass I encountered the error below: npm ERR! notsup Unsupported platform for <a href="/cdn-cgi/l/email ...

Include a bank account for connecting to Stripe custom accounts

Currently, I am implementing Stripe Connect using Node.js and TypeScript for a platform that facilitates payments for third-party services referred to as "partners." Our decision to utilize Stripe Connect's custom accounts gives us complete control ov ...

"Transforming a Vue.js method within a component into a created method: A step-by-step guide

I am currently working on a vue.js codebase where a method within the component is triggered by onclick() However, I would like this method to automatically run when the page loads instead of being called by onclick. After reviewing the vue.js documentati ...

The connection to sockjs-node was refused due to a network error

After setting up a new small web application using vue cli, I encountered an issue right from the start. Here is the error message: (base) marco@pc:~/vueMatters/testproject$ npm run serve > <a href="/cdn-cgi/l/email-protection" class="__cf_email__ ...

Determine if a cookie is set in Vue.js without requiring a page refresh

My current goal with VUE is to make the login url disappear from the navigation bar as soon as the user logs in. After successfully logging in, the token cookie is set in the browser. However, the issue arises when the login url remains visible in the nav ...