Vuex в текущей форме не работает с Typescript. Вероятно, это изменится в Vue 3.
Как и вы, я также не хочу использовать декораторы @Component
, особенно потому, что они устарели. Однако, когда дело доходит до использования стиля компонента Vue typescript по умолчанию:
<script lang="ts">
import Vue from 'vue';
export default Vue.extend({...})
</script>
... после тестирования нескольких решений я обнаружил, что проще всего использовать плагин, который делает использовать декораторы : vuex-module-decorators
Модуль Vuex:
Я обычно оставляю родительское состояние чистым (пустым) и использую модули с пространством имен. Я делаю это в основном потому, что не раз в конце проекта я решал, что было бы чище иметь более одного модуля, и переместить его из родительского модуля в модуль сложнее, чем просто создать дополнительный модуль.
Магазин выглядит так:
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); // this is important for typescript to work properly
export type State = typeof store.state;
export default store;
Вот несколько примеров вычислений mapState
, mapGetters
или get / set, которые работают напрямую с магазином:
computed: {
...mapGetters({
foo: 'whatever/foo',
bar: 'whatever/bar'
}),
...mapState({
prop1: (state: State): prop1Type[] => state.whatever.prop1,
prop2: (state: State): number | null => state.whatever.prop2
}),
// if i want get/set, for a v-model in template
baz: {
get: function(): number {
return this.$store.state.whatever.baz;
},
set: function(value: number) {
if (value !== this.baz) { // read * Note 1
this.$store.dispatch('whatever/setBaz', value);
// setBaz can be an `@Action` or a `@MutationAction`
}
}
}
}
baz
теперь можно использовать в v-model
. Примечание. mapGetters
должны быть фактическими получателями хранилища модулей:
import { $http, $store } from '@/main'; // read * Note 2
import { Action, Module, Mutation, MutationAction, VuexModule } from 'vuex-module-decorators';
@Module({ namespaced: true, store: $store, name: 'whatever' })
export default class Whatever extends VuexModule {
get foo() {
return // something. `this` refers to class Whatever and it's typed
}
baz = 0;
prop1 = [] as prop1Type[]; // here you cast the type you'll get throughout the app
prop2 = null as null | number; // I tend not to mix types, but there are valid cases
// where `0` is to be treated differently than `null`, so...
@MutationAction({ mutate: ['baz'] })
async setBaz(baz: number) {
return { baz }
}
}
Теперь у вас не будет проблем с использованием декораторов @Action
или @Mutation
, и вы можете остановиться на этом, у вас не будет любые проблемы с машинописным текстом. Но поскольку они мне нравятся, я часто использую @MutationAction
sa, хотя, честно говоря, они гибрид. Хак, если хотите.
Внутри @MutationAction
, this
не является классом модуля. Это ActionContext (в основном то, что будет первым параметром в обычном действии js vuex):
interface ActionContext<S, R> {
dispatch: Dispatch;
commit: Commit;
state: S;
getters: any;
rootState: R;
rootGetters: any;
}
И даже не в этом проблема. Проблема в том, что Typescript считает, что this
- это класс модуля внутри @MutationAction
. И вот тогда вам нужно начать кастинг или использовать typeguards. Как правило, я стараюсь свести количество кастингов к минимуму и никогда не использую any
. Typeguards могут go очень далеко.
Золотое правило: если мне нужно использовать as any
или as unknown as SomeType
, это явный знак, что я должен разделить @MutationAction
на @Action
и @Mutation
. Но в подавляющем большинстве случаев достаточно охранника. Пример:
import { get } from 'lodash';
...
@Module({ namespaced: true, store: $store, name: 'whatever' })
export default class Whatever extends VuexModule {
@MutationAction({ mutate: ['someStateProp'] })
async someMutationAction() {
const boo = get(this, 'getters.boo'); // or `get(this, 'state.boo')`, etc...
if (boo instaceof Boo) {
// boo is properly typed inside a typeguard
// depending on what boo is, you could use other typeguards:
// `is`, `in`, `typeof`
}
}
Если вам нужны только значения state
или getters
: this.state?.prop1 || []
или this.getters?.foo
также работают.
Честно говоря, @MutationAction
требует некоторая форма взлома типов, поскольку вам нужно объявить типы: они не выводятся должным образом. Итак, если вы хотите быть на 100% правильным, ограничьте их использование случаями, когда вы просто устанавливаете значение свойства состояния и не хотите писать как действие, так и мутацию:
@MutationAction({ mutate: ['items'] })
async setItems(items: Item[]) {
return { items }
}
Что заменяет:
@Action
setItems(items: Item[]) {
this.context.commit('setItems', items);
// btw, if you want to call other @Action from here or any @MutationAction
// they work as `this.someAction();` or `this.someMutationAction()`;
}
@Mutation
setItems(items: Item[]) {
this.items = items;
}
@MutationAction
s зарегистрированы как @Action
s, они принимают { mutate: [/* full list of props to be mutated*/]}
и возвращают объект, имеющий все объявленные свойства состояния, которые объявлены в массиве свойств.
Вот и все.
* Примечание 1 : мне пришлось использовать эту проверку, когда я использовал два разных входа (обычный и ползунок ввода) на том же get/set
v-model
. Без этой проверки каждый из них при обновлении вызовет set
, что приведет к ошибке переполнения стека. Обычно эта проверка не требуется, если у вас только 1 вход.
* Примечание 2 : вот как мой main.ts
обычно выглядит
import ...
Vue.use(...);
Vue.config...
const Instance = new Vue({
...
}).$mount(App);
// anything I might want to import in components, store modules or tests:
export { $store, $t, $http, $bus } = Instance;
/* I'd say I use these imports more for correct typing than for anything else
(since they're already available on `this` in any component). But they're
quite useful outside of components (in services, helpers, store, translation
files, tests, etc...)
*/