Всем привет!
В этой статье мы поймем, нужно ли вам типизировать Vuex Store или нет, и если вы достаточно отчаянны, поймем, как его типизировать, чтобы не погибнуть.
А нужно ли вам это ?
Тут я бы не советовал выбирать именно Vuex по нескольким причинам:
Vuex больше не будет апдейтится - на главной Vuex это написано - тык, теперь дефолтный State Manager - Pinia. По количеству коммитов на скрине ниже, можно сказать, что пациент мертв)))
Pinia is now the new default
The official state management library for Vue has changed to Pinia. Pinia has almost the exact same or enhanced API as Vuex 5, described in Vuex 5 RFC. You could simply consider Pinia as Vuex 5 with a different name. Pinia also works with Vue 2.x as well.
Vuex почти полностью написан на JS, а что написано на TS, больше похоже на AnyScript. Вот сравнение репозиториев Vuex и Pinia, как доказательство(тут видно, что primary language у Pinia - TS, Vuex - JS).
Если после этого вы все еще хотите типизировать Vuex, то погнали
Начинаем типизацию
Как пример, мы будем писать модуль для типизации Todo приложения.
Типы для нашего TodoModule
Сделаем алиасы для удобного манипулирования типами внутри файла с типами модуля, далее нам это понадобиться.
type State = TodoState;
type Mutations = TodoMutations;
type Getters = TodoGetters;
type Actions = TodoActions;
Типизация State:
Тут все просто, мы просто создаем интерфейс и пишем туда наши свойства:
state.ts
export interface TodoState {
todos: {
name: string;
completed: boolean;
}[];
}
Типизация Mutations
Тут также ничего сложного, мы просто описываем наши Mutations как функции в которые первым параметром передаются State, а вторым опционально передаётся payload:
export interface TodoMutations {
addTodo(state: State, payload: State['todos'][number]): void;
deleteTodo(state: State, payload: State['todos'][number]): void;
setTodos(state: State, payload: State['todos']): void;
}
Типизация Actions
Actions также типизируются как функции с первым параметром, ActionContext({ dispatch, commit, state, getters, rootState, rootGetters }) и вторым параметром опциональным payload.
Здесь я буду использовать типизацию ActionContext из самого пакета Vuex, выглядит она вот так:
Vuex Action Context
export interface ActionContext<S, R> {
dispatch: Dispatch;
commit: Commit;
state: S;
getters: any;
rootState: R;
rootGetters: any;
}
Если вам вдруг недостаточно этой типизации, например вам нужно типизировать getters или rootGeters, то можно сделать самому интерфейс:
Custom Action Context
export interface CustomActionsContext {
dispatch: YourDispatchType;
commit: YourCommitType;
state: YourStateType;
getters: YourGettersType;
rootState: YourRootStateType;
rootGetters: YourRootGettersType;
}
Полученный TodoActions
import { ActionContext } from 'vuex';
import { RootState } from './store';
type MyActionContext = ActionContext<State, RootState>;
export interface TodoActions {
getTodos({ commit }: MyActionContext): Promise<void>;
addTodo({ commit }: MyActionContext, payload: State['todos'][number]): Promise<void>;
deleteTodo({ commit }: MyActionContext, payload: State['todos'][number]): Promise<void>;
}
Типизация Getters
Типизировать Getters чуть сложнее, они типизируются как функции возвращающие какое-то значение, но есть нюансы:
В случае, если у вас в Getters не передается никакой payload, сигнатура выглядит следующим образом
getterName(state: State, getters: Getters, rootState: RootState): ReturnType;
В случае, если в Getters передается payload, сигнатура выглядит вот так
getterName(state: State, getters: Getters, rootState: RootState): (parameters: parametersType) => ReturnType;
Мой полученный TodoGetters
export interface TodoGetters {
getTodosByReadiness(state: State): (isCompleted: boolean) => State['todos'];
}
Типизация Module
Тут получается очень страшная конструкция, которая обогащает базовый тип модуля Vuex Store, по сути мы просто типизируем Mutations, Actions и Getters с помощью полученных нами типов(самое главное тут это переопределение ключей у commit, dispatch, getters, мы сделали так, чтобы они тоже были типизированы)
import { CommitOptions, DispatchOptions, Store as VuexStore } from 'vuex';
export type TodoModule = Omit<VuexStore<State>,
'getters' | 'commit' | 'dispatch'> & {
commit<K extends keyof Mutations, P extends Parameters<Mutations[K]>[1]>(
key: K,
payload?: P,
options?: CommitOptions,
): ReturnType<Mutations[K]>
} & {
dispatch<K extends keyof Actions>(
key: K,
payload?: Parameters<Actions[K]>[1],
options?: DispatchOptions,
): ReturnType<Actions[K]>
} & {
getters: {
[K in keyof Getters]: ReturnType<Getters[K]>
}
}
Итого получаем следующий файл:
types/todo/store.ts
import { CommitOptions, DispatchOptions, Store as VuexStore, ActionContext } from 'vuex';
import { RootState } from './store';
type State = TodoState;
type Mutations = TodoMutations;
type Getters = TodoGetters;
type Actions = TodoActions;
export interface TodoState {
todos: {
name: string;
completed: boolean;
}[];
}
export interface TodoGetters {
getTodosByReadiness(state: State): (isCompleted: boolean) => State['todos'];
}
export interface TodoMutations {
addTodo(state: State, payload: State['todos'][number]): void;
deleteTodo(state: State, payload: State['todos'][number]): void;
setTodos(state: State, payload: State['todos']): void;
}
type MyActionContext = ActionContext<State, RootState>;
export interface TodoActions {
getTodos({ commit }: MyActionContext): Promise<void>;
addTodo({ commit }: MyActionContext, payload: State['todos'][number]): Promise<void>;
deleteTodo({ commit }: MyActionContext, payload: State['todos'][number]): Promise<void>;
}
export type TodoModule = Omit<VuexStore<State>,
'getters' | 'commit' | 'dispatch'> & {
commit<K extends keyof Mutations, P extends Parameters<Mutations[K]>[1]>(
key: K,
payload?: P,
options?: CommitOptions,
): ReturnType<Mutations[K]>
} & {
dispatch<K extends keyof Actions>(
key: K,
payload?: Parameters<Actions[K]>[1],
options?: DispatchOptions,
): ReturnType<Actions[K]>
} & {
getters: {
[K in keyof Getters]: ReturnType<Getters[K]>
}
}
Приступим к имплементации наших интерфейсов
store/modules/todo.ts
// Импортим интерфейсы которые мы ранее написали
import {
TodoActions as Actions,
TodoGetters as Getters,
TodoMutations as Mutations,
TodoState as State,
} from '@/types/todo/store';
// Типы из самого Vuex для совместимости
import { ActionTree, GetterTree, Module, MutationTree } from 'vuex';
import { RootState } from '@/types/store';
// Типизируем State
const state: () => State = () => ({
todos: [],
});
// Типизируем Getter с помощью нашего интерфейса + обертки из Vuex
const getters: GetterTree<State, RootState> & Getters = {
getTodosByReadiness(state) => (isCompleted) => state.todos.filter((todo) => todo.completed === isCompleted);
};
// Типизируем Mutations с помощью нашего типа + обертки из Vuex
const mutations: MutationTree<State> & Mutations = {
addTodo(state, todo) {
state.todos.unshift(todo);
}
deleteTodo(state, todo) {
state.todos = state.todos.filter((t) => t.name !== todo.name)
}
setTodos(state, todos) {
state.todos = todos;
}
};
// Типизируем Actions с помощью нашего типа + обертки из Vuex
const actions: ActionTree<State, RootState> & Actions = {
async getTodos({ commit }) {
const todos = await getTodos();
commit('setTodos', todos);
}
async addTodo({ commit }, todo) {
await addTodo(todo);
commit('addTodo', todo);
}
async deleteTodo({ commit }, todo) {
await deleteTodo(todo);
commit('deleteTodo', todo);
}
};
// Экспортим модуль с типами
const todo: Module<State, RootState> = {
state,
mutations,
getters,
actions,
};
export default todo;
Разбираемся, что выше произошло:
Мы типизировали State как () => State, функция которая возвращает State, все понятно.
Мы типизировали Mutations, Getters, Actions как intersection между нашим типом и типом который предоставляет Vuex из коробки, это делается для того, чтобы мы могли потом прокинуть наши mutations, getters, actions в Module и не получить ошибок.
Экспортировали наш типизированный модуль.
Финальный штрих
Для начала нужно подготовить еще несколько общих типов, для того чтобы все заработало.
types/store.ts
import { TodoState, TodoModule } from '@/types/todo/store';
export interface StateProps {
// Сюда нужно будет прописывать все ваши стейт
state: {
todo: TodoState;
}
}
export type RootState = StateProps['state'];
// Сюда нужно будет добавлять все ваши модули через &
export type StoreType = StateProps & TodoModule;
А теперь объясню, что здесь происходит:
StateProps - Все наши State в одном месте
Из него мы будем брать RootState
Мы будем использовать этот интерфейс для того, чтобы использовать его в нашем типе для Store(он итак должен типизироваться, но у меня по какой-то причине не завелось, поэтому я сделал вот такой костыль)
RootState - Тут понятно
StoreType - Типизация всего нашего стора с State, Mutations, Actions, Getters
Применим это все в нашем Store
Тут мы даем тип инстансу store и хуку useStore, теперь импортируя и используя их, вы получаете типизированный Store.
import { createStore, useStore as VuexStore } from 'vuex';
import { todo } from '@/store/modules/todo';
import { StoreType } from '@/types/store';
const store: StoreType = createStore({
modules: {
todo
},
});
export function useStore(): StoreType {
return VuexStore() as StoreType;
}
export default store;
Чеклист одного успешного Store
Проверяй
Написал типы для своего Store
Написал имплементацию этих типов
Добавил в StateProps и StoreType нужны типы
Типизировал сам Store
Используешь Store/useStore не из Vuex, а из файла, где типизировал Store
Кайфанул от типизации
Если статья показалась вам интересной, то у меня есть Телеграм Канал, где я пишу про новые технологии во фронте, делюсь хорошими книжками и интересными статьями других авторов.
sfirestar
Ответ: перейти на Pinia
Dragonek Автор
Идеальный ответ!