Всем привет!

В этой статье мы поймем, нужно ли вам типизировать 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 грустно в его репо никто не коммитит
Vuex грустно в его репо никто не коммитит

Если после этого вы все еще хотите типизировать 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;

Разбираемся, что выше произошло:

  1. Мы типизировали State как () => State, функция которая возвращает State, все понятно.

  2. Мы типизировали Mutations, Getters, Actions как intersection между нашим типом и типом который предоставляет Vuex из коробки, это делается для того, чтобы мы могли потом прокинуть наши mutations, getters, actions в Module и не получить ошибок.

  3. Экспортировали наш типизированный модуль.

Финальный штрих

Финал близок
Финал близок

Для начала нужно подготовить еще несколько общих типов, для того чтобы все заработало.

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 в одном месте

  1. Из него мы будем брать RootState

  2. Мы будем использовать этот интерфейс для того, чтобы использовать его в нашем типе для 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

Проверяй
  1. Написал типы для своего Store

  2. Написал имплементацию этих типов

  3. Добавил в StateProps и StoreType нужны типы

  4. Типизировал сам Store

  5. Используешь Store/useStore не из Vuex, а из файла, где типизировал Store

  6. Кайфанул от типизации

Успешный успех
Успешный успех

Если статья показалась вам интересной, то у меня есть Телеграм Канал, где я пишу про новые технологии во фронте, делюсь хорошими книжками и интересными статьями других авторов.

Комментарии (2)


  1. sfirestar
    06.10.2023 05:52
    +1

    Ответ: перейти на Pinia


    1. Dragonek Автор
      06.10.2023 05:52

      Идеальный ответ!