Vuex4에서 모델을 만들기 위해 vuex-typing을 만들었습니다.

추기
최근 글로벌 상태를 안전하게 다룰 수 있는 피니아가 등장한 만큼 새로 사용할 때는 일단 논의하는 게 좋다.한편, Vuex에서 이전하는 것은 상당히 어렵기 때문에 기존의 Vuex 자원을 모델로 삼으려면 이 글에서 소개한 vuex-typing과 다른 선택 항목도 선택 항목이 될 수 있다.
업무 중에 Vuex를 사용할 기회가 있는 것 같아서 계속 공부하고 있습니다. 그런데 모델에 대한 지원이 충분하지 않고 불만이 있어서 고정된 조수 라이브러리를 만들었습니다.
  • 창고: https://github.com/d-kimuson/vuex-typing
  • npm: https://www.npmjs.com/package/vuex-typing
  • Vuex의 유형 정의가 부족합니다.


    어플리케이션이 조만간 커질 수 있음을 감안하여 처음부터 모듈로 나누어 관리하는 것이 좋으나 Vuex4에서는 모듈의 모델 지원이 거의 없다
    공식 양식
    export const store = createStore({
      // ...
      modules: {
        counter: {
          namespaced: true,
          state: () => {
            return {
              count: 0
            }
          },
          mutations: {
            increment: (state /* any になる */) => {
              state.count = state.count + 1
            }
          },
          actions: {
            INCREMENT: (context /* any になる */) => {
              context.commit('increment')
            }
          }
        }
      },
    })
    
    이 문제를 해결하기 위해서는 다음과 같은 모듈의 유형을 개별적으로 정의해야 합니다.
    자체 설계
    type CounterState = {
      count: number
    }
    
    type CounterMutation = {
      increment: (state: CounterState) => void
    }
    
    type CounterContext = {
      commit: <Key extends keyof CounterMutation>(  // 試してないので間違ってるかもだけど、概ねこういうの
        key: Key,
        payload?: CounterMutation<typeof key> extends (state: any, payload: infer I) => any
          ? I
          : never
      ) => ReturnType<CounterMutation<typeof key>>,
      /* getters, dispatch も必要 */
    }
    
    type CounterAction = {
      INCREMENT: (context: CounterContext) => Promise<void>
    }
    
    export const counterModule = {
      state: (): CounterState => {
        return {
          count: 0
        }
      },
      mutations: {
        increment: (state: CounterState) => {
          state.count = state.count + 1
        }
      },
      actions: {
        INCREMENT: ({ commit }: CounterContext) => {
          commit('increment')
        }
      }
    }
    
    이렇게 되면 기본적으로 고정된 상태에서 모듈을 쓸 수 있지만 선언문과 실복을 분리해야 하기 때문에 지루해지고 차이가 발생할 때도 유형 정의와 실복을 양방면으로 수정해야 하기 때문에 번거롭다
    모듈뿐만 아니라 정의된 액션스나 Getters를 호출할 때도 모델을 제공하지 않습니다
    const store = useStore()
    store.dispatch('INCREMENT') /* 引数にも戻り値にも型付けされない */
    
    저는 이곳의 모델을 가장 원하기 때문에 이 근처의 모델을 만들 방법을 생각해서 vuex-typing을 만들었습니다.

    가져오기


    npm에서 추가vuex@4vuex-typing
    yarn add vue@next vuex@next vuex-typing
    

    모듈 유형


    vuex-typing에서 모델에 사용되는 함수를 통해 실제 장치에서 유형 정보를 선택하여 모델을 진행합니다
    위의 예는 vuex-typing에서 다음과 같이 쓸 수 있다
    store/modules/counter.ts
    import { defineModule } from 'vuex-typing'
    
    export const counterModuleName = "counter"
    
    const counterModule = defineModule({
      state: () => ({
        count: 0
      }),
      mutations: {
        increment: (state /* 明示せずとも state の型が付く */) => {
          state.count = state.count + 1
        }
      }
    }, {
      // actions
      INCREMENT: ({ commit } /* 明示せずとも context の型が付く */) => {
        commit('increment_typo')  // 型エラー (登録していないミューテーション)
        commit("increment", 20)   // 型エラー (payload の型が異なる)
        commit('increment')       // OK
      }
    })
    
    type CounterModule = typeof counterModule
    
    성형된 거 알아요.

    액션스의mutation 모델만 수집하기 위해 두 번째 파라미터로 드릴게요.
    defineModule에서 actions는 첫 번째 파라미터와 결합하여 namespaced: true를 지정하여 모듈 옵션을 만듭니다.
    전송된 defineModule
    export function defineModule(module, actions) {
      return {
        ...module,
        namespaced: true,
        actions: actions,
      };
    }
    
    따라서 반환 값을 Vuex Store에 직접 전달함으로써 모듈을 등록할 수 있습니다.
    ※ 모델이 복잡해져 강제 진행namespaced: true

    store 모델


    Vuex4의use Store에서store는 모델이 있지만 처음에 쓴 것처럼 gettersdispatch에 모델을 추가할 수 없기 때문에 vuex-typingTypedStore로 유형 정의를 생성하고 정식적으로
    TypeScript Support | Vuex
    덮어쓰기 형식 정의useStore 정의
    store/index.ts
    import { createStore } from "vuex"
    import { TypedStore } from "vuex-typing"
    
    import { counterModuleName, counterModule } from "./modules/counter"
    
    export type RootState = {}
    export type ModuleType = {
      [counterModuleName]: typeof counterModule
    }
    
    export type RootStore = TypedStore<RootState, ModuleType>
    
    export const store = createStore<RootState>({
      state: {
        rootVal: "ok",
      },
      modules: {
        [counterModuleName]: counterModule,
      },
    })
    
    store/util.ts
    import { InjectionKey } from "vue"
    import { useStore as baseUseStore } from "vuex"
    import type { RootStore } from "."
    
    export const key: InjectionKey<RootStore> = Symbol()
    
    export function useStore(): RootStore {
      return baseUseStore(key)
    }
    
    이렇게 하면 독자적으로 정의된useStore 포맷된 상태dispatch 또는 모듈의 상태를 사용할 수 있다
    sample-component.ts
    import { useStore } from '~/store/util'
    
    const store = useStore()
    
    store.state.counter.count  // :number
    const result /* :Promise<void> */ = store.dispatch('counter/INCREMENT')  // OK
    store.dispatch('counter/INCREMENT_TYPO')  // 型エラー
    store.dispatch('counter/INCREMENT', 20)   // payload の型エラー
    
    또한 TypedStore에 정의된 유형을 사용하여 Option API를 포맷할 수 있습니다.
    참조: TypeScript Support | Vuex#typing-store-property-in-vue-component
    @types/vuex.d.ts
    import type { RootStore } from "../src/store/index"
    
    declare module "@vue/runtime-core" {
      interface ComponentCustomProperties {
        $store: RootStore
      }
    }
    
    이렇게 해서 this.$store도 모델이 생겼어요.

    mapHelpers 사용


    맵Helpers(maptate,mapGetters,mapActions)에서도 모델을 진행할 수 있습니다.useStore 마찬가지로 vuex-typing의 형식으로 덮어쓰기 형식을 정의합니다.
    store/util.ts
    import {
      mapGetters as baseMapGetters,
      mapState as baseMapState,
      mapActions as baseMapActions,
    } from "vuex"
    import { MapState, MapGetters, MapActions } from "vuex-typing"
    import type { RootStore, ModuleType } from "."
    
    export const mapState = baseMapState as unknown as MapState<
      RootStore["state"],
      ModuleType
    >
    export const mapGetters = baseMapGetters as unknown as MapGetters<ModuleType>
    export const mapActions = baseMapActions as unknown as MapActions<ModuleType>
    
    구성 요소는 덮어쓰기 형식 정의의 맵Helpers 함수를 사용합니다
    sample-component.ts
    import { defineComponent } from "vue"
    import { mapState, mapGetters, mapActions } from "../store/util"
    
    defineComponent({
      computed: {
        ...mapState("counter", {
          count: (state /* 型付けされてる */) => state.count,
        }),
        // モジュール名、プロパティ('cnt', 'PLUS_N' 等)が間違っていたら型エラーに
        ...mapGetters("counter", ["cnt"]),
        ...mapGetters(["counter/cnt"]),
      },
      methods: {
        ...mapActions(["counter/INCREMENT"]),
        ...mapActions("counter", ["PLUS_N"]),
        test() {
          // mapState
          this.count // :number
    
          // mapGetters
          this["counter/cnt"] // :number
          this.cnt // :number
    
          // mapActions
          this["counter/INCREMENT"]()
          this.PLUS_N(20)
        },
      },
    })
    

    자신의 Getters와 actions를 참조하세요.


    Vuex의 모듈에서는 모듈에 발표된 다른 getters, actions를 사용할 수 있지만, 순환 인용이기 때문에 이 유형을 진행할 수 없습니다.명확한 형식으로 대체할 수 있다
    import { defineModule, LocalGetters, LocalDispatch } from "vuex-typing"
    
    export const counterModule = defineModule({
      getters: {
        cnt: (state) => state.count,
        cnt2: (_state, _getters): number /* 循環参照になるので明示する必要がある */ => {
          const getters = _getters as LocalGetters<CounterModule["getters"]>  // 上書き
          return getters.cnt
        },
      },
      {
        INCREMENT: ({ commit }): void => {
          commit("increment")
        },
        PLUS_N_LOOP: ({ dispatch: _dispatch }, n: number) => {
          const dispatch: LocalDispatch<CounterModule["actions"]> = _dispatch  // 上書き
    
          for (const _i of new Array(n)) {
            dispatch("INCREMENT")
          }
        },
      }
    })
    
    type CounterModule = typeof counterModule
    
    이상이 주요한 사용 방법이다.
    모든 샘플이 정식 창고의 example에 있습니다.yarn create @vitejs/app example --template vue-ts에 생성된 보일러판에 vuex-typing의 예를 놓아 복제 후 직접 테스트할 수 있다.

    할 수 없는 일


    Vuex의 사용 방법은 매우 유연하다. 한마디로 전역 상태를 관리하는 것이고 여러 가지 상태 관리, 호출 방법도 있다. 모두 포맷할 수 없기 때문에 기능을 삭감하고 있다.
  • Getter에서 전역 상태 및 트위터 사용
  • someGetter(state, getters, rootState, rootGetters)에서는 getters, 루트 State, 루트 Getters에 접근할 수 있으나 이 모델을 진행할 수 없습니다(루트 State와 getters만 형식 정의를 명시하고 덮어쓸 수 있습니다)
  • 참조: 모듈 | Vuex# 네임스페이스 모듈의 글로벌 자산 액세스

  • nampespaced: true 모듈
  • 중첩 모듈
  • 모듈에서 모듈을 선언할 수도 있지만 모델 처리가 불가능하므로 지원되지 않음
  • 참조: 모듈 | Vuex# 네임스페이스
  • 전 세계 트위터, 음소거 및 동작 유형
  • createStore는 상태 정보만 줍기 때문에 줍으려면createStore의 유형 정의 등을 덮어써야 하기 때문에 대응하지 않습니다
  • 컨디션이 먼저 주울 수 있어서 정형화
  • 따라서 vuex-typing를 사용할 때 모든 상태를 모듈식으로 관리하는 것이 좋다(전 세계에서 상태나 동작 등이 일어나지 않는다).(일관성이 있는 독자는 읽을 때도 쉽게 읽을 수 있다)

    기타 유형 옵션


    프로그램 라이브러리를 만들기 전에 여러 가지 종류의 옵션을 찾아보았기 때문에 총괄해 보겠습니다.

    Vuex5


    Vuex5의 경우 API가 많이 바뀔 것 같지만 TypeScript의 fulsa 포트가 들어가기 때문에 앞으로 Vuex5가 출시되면 새로운 Vuex를 추가하면 차분하게 Vuex5의 스타일링을 하는 것이 좋습니다.
    하지만 API가 많이 변했기 때문에 기존 Vuex에서 시작하는 마이그레이션은 어려울 것 같습니다.
    참조: Vuex5로 어떻게 변해요?

    ktsn/vuex-type-helper


    처음에 쓴 Context 등 형식 정의를 준비한 ktsn/vuex-type-helper 형식의 조수 라이브러리가 있습니다.
    필기 정의와 실복을 분리해야 한다는 점은 변하지 않지만, 콘텍스트 등 장편 장편 장르의 조수로서 준비돼 있기 때문에 자신보다 준비하는 것이 좋다.
    이 글에서 사용한 예vuex-type-helper는 다음과 같다.
    견본
    import * as Vuex from 'vuex'
    import { DefineMutations, DefineActions, Dispatcher, Committer } from 'vuex-type-helper'
    
    // 型定義
    export interface CounterState {
      count: number
    }
    
    export interface CounterMutation {
      increment: void
    }
    
    export interface CounterActions {
      INCREMENT: void
    }
    
    // 実装
    const state: CounterState = {
      count: 0
    }
    
    const mutations: DefineMutations<CounterMutations, CounterState> = {
      increment (state) {
        state.count = statel.count + 1
      }
    }
    
    const actions: DefineActions<CounterActions, CounterState, CounterMutations, CounterGetters> = {
      INCREMENT ({ commit }) {
        commit('increment')
      }
    }
    

    paroi-tech/direct-vuex


    paroi-tech/direct-vuex라는 프로그램 라이브러리가 있는데, 여기서도 vuex-typing와 같은 방법으로 모델을 진행하였다.
    느낌은 좋지만 Vuex 기준 이외의 동작과 트위터 호출, 점포 로그인 방법도 좀 달라요. 허락해주셨으면 좋겠어요.
    견본
    store.dispatch("mod1/myAction", myPayload)  // Vuex 標準
    store.dispatch.mod1.myAction(myPayload)     // direct-vuex での書き方
    
    나는 이미 있는 것 같아서 찾아보니 이 프로그램 라이브러리를 발견하였다스타일 지침은 좋지만 조만간 Type Script의 지원이 있을 것을 고려해 기준을 벗어나고 싶지 않아 했다vuex-typing(Vuex5에서 API가 바뀌었다고 하지만 기준에 따라 하면 이전 원가가 비교적 낮을 것)

    끝맺다


    이상vuex-typing의 소개입니다.
    새로운 Vuex를 사용할 기회가 있다면 꼭 사용해 주세요.

    참고 자료

  • Vuex란? |Vuex
  • Vuex5로 어떻게 변해요?
  • GitHub - ktsn/vuex-type-helper: Type level helper to ensure type safety in Vuex
  • GitHub - paroi-tech/direct-vuex: Use and implement your Vuex store with TypeScript types. Compatible with the Vue 3 composition API.
  • Composition API+Type Script로 Vuex4 시작하기
  • 좋은 웹페이지 즐겨찾기