vuejs / vuex

🗃️ Centralized State Management for Vue.js.
https://vuex.vuejs.org
MIT License
28.4k stars 9.56k forks source link

Feature: Manually install `vuex/types/vue.d.ts` #994

Closed blake-newman closed 4 years ago

blake-newman commented 6 years ago

This is a breaking change

What problem does this feature solve?

With module merging in TS, we can override module interfaces. However it is limited, in the case of the Vuex types it's impossible to compose full typing layer in components.

If we make it optional to install vuex/types/vue.d.ts then we can do our own manual declaration. Which will enable fully typed structures. I would also rename to vuex/types/install.d.ts

What does the proposed API look like?

Once we allow custom installation we can do something similar to this to enable full typing layer.

import Vue from 'vue'
import * as Vuex from 'vuex'
import { RootState } from 'store/types'

Vue.use(Vuex);

export class Store {
  store: Vuex.Store<RootState>;

  static instance: Store;

  constructor() {
    this.store = new Vuex.Store<RootState>{
      strict: true
    });
  }

  get() {
    return this.store
  }
}

declare module "vue/types/options" {
  interface ComponentOptions<V extends Vue> {
    store?: Vuex.Store<RootState>;
  }
}

declare module "vue/types/vue" {
  interface Vue {
    $store: Vuex.Store<RootState>;
  }
}

export default Store
ktsn commented 6 years ago

I agree that we should manually declare $store on Vue instance type. But I also think there are no need to do that for ComponentOptions. It might be better to separate instance type augmentation and component options type augmentation.

mitchell-garcia commented 6 years ago

This would be a huge boost in productivity for us Typescript users - current examples are extremely verbose, especially when it comes to writing getters. In order to get static typing on any namespaced property from the $store, you have to write a getter and a storeAccessor (provided by vuex-typescript) for it.

import { getStoreAccessors } from 'vuex-typescript';

export const getters = {
    getProperty(state: ExampleState) {
        return state.property;
    }
}

const { read } = getStoreAccessors<ExampleState, ExampleRootState>('example');

export const getProperty= read(getters.getProperty);

Being able to specify the structure for this.$store on a per-project basis would save so many hours of work, I would like to pick this up if it's agreed that it would be the right direction.

yyx990803 commented 6 years ago

I think this is a good idea, my only concern is backwards compatibility since we just released a new major version. Is there anyway to tell TS not to use the included types?

blake-newman commented 6 years ago

As far as I can see no, looks like it'll be a complete breaking change. If doing that we may aswell plan to improve typing layer where we can.

@ktsn has done some awesome work over here. https://github.com/ktsn/vuex-type-helper

skovmand commented 6 years ago

+1 for this.

I came here to start the same discussion.

We currently solve this by manually type casting this.$store to our own store interface to get typed variables from the store. It gets pretty verbose writing (<OurStore>this.$store). If we could define type of this.$store globally, it would solve this.

interface OurState {
  search: {
    query: string | null;
    results: ImageSearchResult[];
    viewType: boolean;
  }
}

interface OurStore extends Store<OurState> {}

export default Vue.extend({
  name: "search-result",
  template: searchResultTemplate,
  computed: {
    results(): ImageSearchResult[] {
      return (<OurStore>this.$store).state.search.results;
    },
    viewType() {
      return (<OurStore>this.$store).state.search.viewType;
    }
  }
})
blake-newman commented 6 years ago

I will move this to a discussion thread, as there is more we can do to make the Vuex API more type friendly.

skovmand commented 6 years ago

Sounds good. Will the discussion remain here, or can you give us a link to it?

blake-newman commented 6 years ago

I will add a link, forming the thread atm including details on current issues.

blake-newman commented 6 years ago

@ktsn Has done some great work in getting Vuex more type safe. Thus this issue can be solved with his work, as we can do a major semver upgrade

ysabri commented 6 years ago

What is the state on adding this to the library?

ZSkycat commented 6 years ago

tsconfig.json

{
    "file": [
        "./node_modules/vue-router/types/vue.d.ts",
        "./node_modules/vuex/types/vue.d.ts",
    ],
    "include": [
        "./src/**/*",
    ],
}

I have always import the description file manually. Is your automatic import?

I actually prefer to use import. I may have multiple entry files, multiple vuex instances.

import { store } from 'store.ts'

Change the type as you like

export default store as YourType
therealmikz commented 5 years ago

Any plans on this? I've struggled whole morning to try to override this declaration and found no way to achieve this.

michalsnik commented 5 years ago

@therealmikz I think that the only way to patch this at this moment is to use https://www.npmjs.com/package/patch-package

It's not the best solution I know, but at least it works and makes your store state typed until it's officially solved.

therealmikz commented 5 years ago

@michalsnik thanks, I've seen this one before and I was planning to check it out

Edit: it worked. It's definitely not an elegant solution, but it does the job Btw. I recommend not to change Store<any> to Store<MyState> inside of vue.d.ts, but to remove this entirely and put typings into project code.

Niedzwiedzw commented 4 years ago

what's the recommended solution to allow for Store<MyState> instead of Store<any> now?

budziam commented 4 years ago

I would suggest going with a new, custom field that returns $store instance, but can be strongly typed.

Object.defineProperty(Vue.prototype, "$stock", {
    get(): Store<RootState> {
        return this.$store;
    }
});

declare module "vue/types/vue" {
    interface Vue {
        $stock: Store<RootState>;
    }
}

Now, all you need to do is to replace usage of this.$store with this.$stock everywhere.

kiaking commented 4 years ago

This issue has been solved at the 4.0.0-beta.1 🎉 https://github.com/vuejs/vuex/releases/tag/v4.0.0-beta.1

andrewvasilchuk commented 4 years ago

Checkout this article Vuex + TypeScript.

cefn commented 2 years ago

If a workaround is still valuable (for those locked to a Vue2 codebase, for example), I found the following to be useful.

It wraps the Vuex' builtin mapState, mapping from this.$store to named properties on this but inferring the correct type of the computed methods from the State type you pass.

import { mapState } from "vuex";

export function mapTypedState<State>(keys: (string & keyof State)[]) {
  type Key = typeof keys[number];
  return mapState(keys) as {
    [key in Key]: () => State[key];
  };
}

In your store definition you can create a type-specific call like this if you want...

export function mapRootState(keys: (keyof RootState)[]) {
  return mapTypedState<RootState>(keys);
}

This can very tersely, and type-safely, construct locally mapped properties from the store's state. Note Vuex mapState calls can be namespaced if you want to pick out typed state from within a module.

The code example below constrains the type of this.message to string, and Volar tooling can autocomplete the name 'message' and the method .split() within my HelloWorld.vue component (see the PR against a repro of the Vue2 problem case)...

import { mapRootState } from "@/store";
import Vue from "vue";

export default Vue.extend({
  name: "HelloWorld",
  computed: {
    ...mapRootState(["message"]),
    backwardsMessage(): string {
      const chars = this.message.split("");
      chars.reverse();
      return chars.join("");
    },
  },
});