V

Vuex + TypeScript

2025-06-04

Vuex + TypeScript

前言

4.x🚨 在 Vuex和 Vue.js3.x完全发布之前,不建议在生产环境中使用本文描述的方法。Vuex4.x和 Vue.js 3.xAPI 仍然不稳定。本文仅演示了我对 Vuex store 进行静态类型化的尝试,因为 Vuex storeVuex@v4.0.0-beta.1已经移除了它的全局类型。


⚠️ 项目配置部分被有意省略。所有源代码都位于此仓库中。

介绍

Vuex@v4.0.0-beta.1正式发布。其中一项重大变化是,该库不再附带this.$storeVue 组件内的全局类型。

this.$storeVuex 4 删除了Vue 组件中的全局类型

您可以在本期找到有关其背后的原因和动机的更多信息

由于全局类型已被移除,开发者需要自行定义。如发行说明中所述:

使用 TypeScript 时,您必须提供自己的增强声明。

在本文中,我想分享我扩充 store 类型的经验。我将通过一个简单 store 的示例来演示这一点。为了简单起见,我们的 store 尽可能地简单。

让我们进行一些编码。

状态

商店的定义始于状态的定义。

state.ts



export const state = {
  counter: 0,
}

export type State = typeof state


Enter fullscreen mode Exit fullscreen mode

我们需要导出状态类型,因为它将用于 getter、mutations 和 action 的定义。

到目前为止一切顺利。让我们继续讨论突变。

突变

正如Vuex 文档中所述

在各种 Flux 实现中,使用常量作为突变类型是一种常见的模式。

因此,我们所有可能的突变名称都将存储在MutationTypes枚举中。

mutation-types.ts



export enum MutationTypes {
  SET_COUNTER = 'SET_COUNTER',
}



Enter fullscreen mode Exit fullscreen mode

现在我们已经定义了突变的名称,我们可以为每个突变(其实际类型)声明一个契约。突变只是一个简单的函数,它接受状态作为第一个参数,有效载荷作为第二个参数,并最终改变前者。State类型在执行时用作第一个参数的类型。第二个参数特定于特定的突变。我们已经知道突变的存在SET_COUNTER,所以让我们为它声明类型。

mutations.ts



import { MutationTypes } from './mutation-types'
import { State } from './state'

export type Mutations<S = State> = {
  [MutationTypes.SET_COUNTER](state: S, payload: number): void
}


Enter fullscreen mode Exit fullscreen mode

太棒了!现在是时候实现它了。



import { MutationTree } from 'vuex'
import { MutationTypes } from './mutation-types'
import { State } from './state'

export type Mutations<S = State> = {
  [MutationTypes.SET_COUNTER](state: S, payload: number): void
}

export const mutations: MutationTree<State> & Mutations = {
  [MutationTypes.SET_COUNTER](state, payload: number) {
    state.counter = payload
  },
}


Enter fullscreen mode Exit fullscreen mode

mutations变量负责存储所有已实现的突变,并最终将用于构建存储。

MutationTree<State> & Mutations类型交集保证了契约的正确实现。如果没有正确实现,TypeScript 会报错,并出现以下错误:

TypeScript 抱怨未实现的契约



Type '{ SET_COUNTER(state: { counter: number; }, payload: number): void; }' is not assignable to type 'MutationTree<{ counter: number; }> & Mutations<{ counter: number; }>'.
  Property '[MutationTypes.RESET_COUNTER]' is missing in type '{ SET_COUNTER(state: { counter: number; }, payload: number): void; }' but required in type 'Mutations<{ counter: number; }>'


Enter fullscreen mode Exit fullscreen mode

MutationTree关于类型,简单说几句。MutationTree它是一个泛型类型,随包一起提供vuex。从名字就可以看出,它用于声明一个类型的变异树。

vuex/types/index.d.ts



export interface MutationTree<S> {
  [key: string]: Mutation<S>;
}


Enter fullscreen mode Exit fullscreen mode

但它不够具体,无法满足我们的需求,因为它假设突变的名称可以是任意的string,但在我们的例子中,我们知道突变的名称只能是typeof MutationTypes。我们保留此类型只是为了与Store选项兼容。

行动

对于这种简单的存储,不需要任何操作,但是为了说明操作的类型,让我们想象一下我们可以从某个地方获取计数器。

我们存储突变名称的方式与存储动作名称的方式相同。

action-types.ts



export enum ActionTypes {
  GET_COUTNER = 'GET_COUTNER',
}


Enter fullscreen mode Exit fullscreen mode

actions.ts



import { ActionTypes } from './action-types'

export const actions = {
  [ActionTypes.GET_COUTNER]({ commit }) {
    return new Promise((resolve) => {
      setTimeout(() => {
        const data = 256
        commit(MutationTypes.SET_COUNTER, data)
        resolve(data)
      }, 500)
    })
  },
}


Enter fullscreen mode Exit fullscreen mode

我们有一个简单的GET_COUNTER操作,它返回Promise,并在 500 毫秒内完成解析。它提交了之前定义的变更(SET_COUNTER)。一切看起来都正常,但commit它允许提交任何变更,这很不合适,因为我们知道 只能提交已定义的变更。让我们来修复这个问题。



import { ActionTree, ActionContext } from 'vuex'
import { State } from './state'
import { Mutations } from './mutations'
import { ActionTypes } from './action-types'
import { MutationTypes } from './mutation-types'

type AugmentedActionContext = {
  commit<K extends keyof Mutations>(
    key: K,
    payload: Parameters<Mutations[K]>[1]
  ): ReturnType<Mutations[K]>
} & Omit<ActionContext<State, State>, 'commit'>

export interface Actions {
  [ActionTypes.GET_COUTNER](
    { commit }: AugmentedActionContext,
    payload: number
  ): Promise<number>
}

export const actions: ActionTree<State, State> & Actions = {
  [ActionTypes.GET_COUTNER]({ commit }) {
    return new Promise((resolve) => {
      setTimeout(() => {
        const data = 256
        commit(MutationTypes.SET_COUNTER, data)
        resolve(data)
      }, 500)
    })
  },
}


Enter fullscreen mode Exit fullscreen mode

就像我们声明突变契约一样,我们也声明了动作契约(Actions)。我们还必须扩充包ActionContext中附带的类型vuex,因为它假设我们可以提交任何突变。AugmentedActionContext完成这项工作,限制仅提交已声明的突变(它还会检查有效载荷类型)。

在操作中输入commit

类型提交

实施不当的行动:

行动实施不当

吸气剂

Getter 也可以是静态类型的。Getter 类似于 Mutation,本质上是一个接收状态作为第一个参数的函数。Getter 的声明与 Mutation 的声明没有太大区别。

getters.ts



import { GetterTree } from 'vuex'
import { State } from './state'

export type Getters = {
  doubledCounter(state: State): number
}

export const getters: GetterTree<State, State> & Getters = {
  doubledCounter: (state) => {
    return state.counter * 2
  },
}


Enter fullscreen mode Exit fullscreen mode

全局$store类型

商店的核心模块已经定义完毕,现在我们可以实际构建商店了。 中的商店创建流程Vuex@v4.0.0-beta.1与 略有不同Vuex@3.x。更多信息请参阅发行说明Store应声明 类型以便在组件中安全地访问定义的商店。请注意,默认的 Vuex 类型:getterscommitdispatch应替换为我们之前定义的类型。进行这种替换的原因是默认的 Vuex 商店类型过于通用。只需查看默认的 getter 类型:



export declare class Store<S> {
  // ...
  readonly getters: any;
  // ...
}


Enter fullscreen mode Exit fullscreen mode

毫无疑问,如果您想安全地使用类型化商店,这些类型并不合适。

store.ts



import {
  createStore,
  Store as VuexStore,
  CommitOptions,
  DispatchOptions,
} from 'vuex'
import { State, state } from './state'
import { Getters, getters } from './getters'
import { Mutations, mutations } from './mutations'
import { Actions, actions } from './actions'

export const store = createStore({
  state,
  getters,
  mutations,
  actions,
})

export type Store = 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]>
  }
}


Enter fullscreen mode Exit fullscreen mode

我不会关注 TypeScript 的实用类型

我们已经到达终点线。剩下的就是扩充全局 Vue 类型了。

types/index.d.ts



import { Store } from '../store'

declare module '@vue/runtime-core' {
  interface ComponentCustomProperties {
    $store: Store
  }
}


Enter fullscreen mode Exit fullscreen mode

太棒了!我们准备好享受完全打字的商店访问了。

在组件中的使用

现在我们的 store 已正确声明并静态类型化,我们可以在组件中使用它了。我们将介绍如何在使用 Options API 和 Composition API 语法定义的组件中使用 store,因为 Vue.js 3.0 两者都支持。

选项 API



<template>
  <section>
    <h2>Options API Component</h2>
    <p>Counter: {{ counter }}, doubled counter: {{ counter }}</p>
    <input v-model.number="counter" type="text" />
    <button type="button" @click="resetCounter">Reset counter</button>
  </section>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
import { MutationTypes } from '../store/mutation-types'
import { ActionTypes } from '../store/action-types'

export default defineComponent({
  name: 'OptionsAPIComponent',
  computed: {
    counter: {
      get() {
        return this.$store.state.counter
      },
      set(value: number) {
        this.$store.commit(MutationTypes.SET_COUNTER, value)
      },
    },
    doubledCounter() {
      return this.$store.getters.doubledCounter
    }
  },
  methods: {
    resetCounter() {
      this.$store.commit(MutationTypes.SET_COUNTER, 0)
    },
    async getCounter() {
      const result = await this.$store.dispatch(ActionTypes.GET_COUTNER, 256)
    },
  },
})
</script>


Enter fullscreen mode Exit fullscreen mode

键入state
类型化状态

键入getters
类型化 getters

键入commit
类型提交

键入dispatch
类型调度

组合 API

要在使用 Composition API 定义的组件中使用存储,我们必须通过useStore钩子访问它,它只返回我们的存储:



export function useStore() {
  return store as Store
}


Enter fullscreen mode Exit fullscreen mode


<script lang="ts">
import { defineComponent, computed, h } from 'vue'
import { useStore } from '../store'
import { MutationTypes } from '../store/mutation-types'
import { ActionTypes } from '../store/action-types'

export default defineComponent({
  name: 'CompositionAPIComponent',
  setup(props, context) {
    const store = useStore()

    const counter = computed(() => store.state.counter)
    const doubledCounter = computed(() => store.getters.doubledCounter)

    function resetCounter() {
      store.commit(MutationTypes.SET_COUNTER, 0)
    }

    async function getCounter() {
      const result = await store.dispatch(ActionTypes.GET_COUTNER, 256)
    }

    return () =>
      h('section', undefined, [
        h('h2', undefined, 'Composition API Component'),
        h('p', undefined, counter.value.toString()),
        h('button', { type: 'button', onClick: resetCounter }, 'Reset coutner'),
      ])
  },
})
</script>


Enter fullscreen mode Exit fullscreen mode

键入state
类型化状态

键入getters
类型化 getters

键入commit
类型提交

键入dispatch
类型调度

结论

我们努力的成果是完全静态类型的存储。我们只允许提交/分派已声明的、带有适当负载的变更/操作,否则会报错。

目前 Vuex 尚未提供正确的辅助函数来简化存储类型处理,因此我们必须手动操作。希望 Vuex 的后续版本能够支持灵活的存储类型。

文章来源:https://dev.to/3vilarthas/vuex-typescript-m4j
PREV
在构建时使用 Tailwind 和 lit-element
NEXT
GitHub vs GitLab Git GitHub GitLab GitLab vs GitHub- 细微功能 -> 哦!现在到了我们胜出的时候了