探讨 Vuex 或 Pinia 在 TypeScript 项目中的类型安全实践,包括 `State`, `Getters`, `Mutations`, `Actions` 的类型声明。

各位观众老爷,大家好!今天咱们来聊聊 TypeScript 项目中 Vuex 和 Pinia 的类型安全那些事儿。这年头,写前端项目,类型安全那可是基本素养,谁也不想上线了才发现 undefined 满天飞,对吧?

咱们先从 Vuex 开始,这玩意儿在 Vue 2 时代可是扛把子,虽然现在 Pinia 势头很猛,但 Vuex 依然有很多项目在使用。

Vuex 的类型安全实践

Vuex 的核心概念是:State、Getters、Mutations、Actions。要在 TypeScript 中玩转 Vuex,核心就是给这四个家伙安排上合适的类型。

1. State 的类型声明

State 就是咱们的数据中心,里面放着各种状态。类型声明当然得安排上,不然编辑器都没法给你提示。

// src/store/types.ts (专门放类型定义的文件是个好习惯)
export interface RootState {
  count: number;
  message: string;
  user: {
    id: number;
    name: string;
  } | null;
}

上面定义了一个 RootState 接口,里面包含了 count (number), message (string) 和 user (对象或 null)。

2. Getters 的类型声明

Getters 就像是 State 的计算属性,从 State 派生出新的数据。类型声明也必须跟上。

// src/store/types.ts
export interface RootGetters {
  doubleCount: (state: RootState) => number;
  userName: (state: RootState) => string;
}

这里定义了两个 Getter 的类型,doubleCount 返回 numberuserName 返回 string。注意,Getter 函数接收 state 作为参数,类型就是 RootState

3. Mutations 的类型声明

Mutations 是唯一允许修改 State 的地方,必须是同步的。类型声明也很重要,可以避免误操作。

// src/store/types.ts
export enum MutationTypes {
  INCREMENT = 'INCREMENT',
  DECREMENT = 'DECREMENT',
  SET_USER = 'SET_USER',
  UPDATE_MESSAGE = 'UPDATE_MESSAGE', // 新增一个 Mutation 类型
}

export interface MutationsInterface {
  [MutationTypes.INCREMENT](state: RootState, payload: number): void;
  [MutationTypes.DECREMENT](state: RootState, payload: number): void;
  [MutationTypes.SET_USER](state: RootState, payload: { id: number; name: string } | null): void;
  [MutationTypes.UPDATE_MESSAGE](state: RootState, payload: string): void; // 新增 Mutation 类型对应的类型定义
}

这里定义了一个 MutationTypes 枚举,用于管理 Mutation 的名称,避免写错。然后定义了一个 MutationsInterface 接口,描述了每个 Mutation 的参数类型和返回值类型。注意,Mutation 函数接收 statepayload 作为参数,state 的类型是 RootStatepayload 的类型根据实际情况定义。

4. Actions 的类型声明

Actions 用于处理异步操作,可以提交 Mutations 来修改 State。类型声明也必不可少。

// src/store/types.ts
export enum ActionTypes {
  INCREMENT_ASYNC = 'INCREMENT_ASYNC',
  FETCH_USER = 'FETCH_USER',
  UPDATE_MESSAGE_ASYNC = 'UPDATE_MESSAGE_ASYNC', // 新增一个 Action 类型
}

export interface ActionsInterface {
  [ActionTypes.INCREMENT_ASYNC](context: AugmentedActionContext, payload: number): Promise<void>;
  [ActionTypes.FETCH_USER](context: AugmentedActionContext): Promise<void>;
  [ActionTypes.UPDATE_MESSAGE_ASYNC](context: AugmentedActionContext, payload: string): Promise<void>; // 新增 Action 类型对应的类型定义
}

// 为 `commit` 增加类型定义
type AugmentedActionContext = Omit<ActionContext<RootState, RootState>, 'commit'> & {
  commit<K extends keyof MutationsInterface>(
    key: K,
    payload: Parameters<MutationsInterface[K]>[1] // 获取 payload 的类型
  ): ReturnType<MutationsInterface[K]>;
};

这里定义了一个 ActionTypes 枚举,用于管理 Action 的名称。然后定义了一个 ActionsInterface 接口,描述了每个 Action 的参数类型和返回值类型。注意,Action 函数接收 context 作为参数,context 包含 statecommitdispatch 等属性。为了类型安全,我们需要对 context 进行增强,特别是 commit 方法,确保提交的 Mutation 名称和 payload 类型是正确的。

5. 创建 Vuex Store 并应用类型

// src/store/index.ts
import Vue from 'vue';
import Vuex, { StoreOptions, ActionContext } from 'vuex';
import { RootState, RootGetters, MutationsInterface, ActionsInterface, MutationTypes, ActionTypes } from './types';

Vue.use(Vuex);

const state: RootState = {
  count: 0,
  message: 'Hello Vuex!',
  user: null,
};

const getters: RootGetters = {
  doubleCount: (state) => state.count * 2,
  userName: (state) => state.user ? state.user.name : 'Guest',
};

const mutations: MutationsInterface = {
  [MutationTypes.INCREMENT](state, payload) {
    state.count += payload;
  },
  [MutationTypes.DECREMENT](state, payload) {
    state.count -= payload;
  },
  [MutationTypes.SET_USER](state, payload) {
    state.user = payload;
  },
  [MutationTypes.UPDATE_MESSAGE](state, payload) {
    state.message = payload;
  }
};

const actions: ActionsInterface = {
  async [ActionTypes.INCREMENT_ASYNC]({ commit }, payload) {
    await new Promise((resolve) => setTimeout(resolve, 1000));
    commit(MutationTypes.INCREMENT, payload);
  },
  async [ActionTypes.FETCH_USER]({ commit }) {
    // 模拟异步请求
    await new Promise((resolve) => setTimeout(resolve, 500));
    const user = { id: 1, name: 'John Doe' };
    commit(MutationTypes.SET_USER, user);
  },
  async [ActionTypes.UPDATE_MESSAGE_ASYNC]({ commit }, payload) {
    await new Promise(resolve => setTimeout(resolve, 500));
    commit(MutationTypes.UPDATE_MESSAGE, payload);
  }
};

const storeOptions: StoreOptions<RootState> = {
  state,
  getters,
  mutations,
  actions,
};

const store = new Vuex.Store<RootState>(storeOptions);

export default store;

这里创建了一个 Vuex Store,并且将之前定义的类型应用到了 State、Getters、Mutations 和 Actions 上。注意,创建 Vuex.Store 时,需要传入 StoreOptions<RootState>,指定根状态的类型。

6. 在组件中使用 Vuex

// src/components/MyComponent.vue
<template>
  <div>
    <p>Count: {{ count }}</p>
    <p>Message: {{ message }}</p>
    <p>User Name: {{ userName }}</p>
    <button @click="increment(1)">Increment</button>
    <button @click="decrement(1)">Decrement</button>
    <button @click="incrementAsync(2)">Increment Async</button>
    <button @click="fetchUser">Fetch User</button>
    <input type="text" v-model="newMessage">
    <button @click="updateMessageAsync">Update Message Async</button>
  </div>
</template>

<script lang="ts">
import { Component, Vue } from 'vue-property-decorator';
import { mapState, mapGetters, mapActions } from 'vuex';
import { RootState, RootGetters, ActionsInterface, ActionTypes } from '@/store/types';

@Component({
  computed: {
    ...mapState(['count', 'message']),
    ...mapGetters(['doubleCount', 'userName']),
  },
  methods: {
    ...mapActions(['incrementAsync', 'fetchUser']),
  },
})
export default class MyComponent extends Vue {
  newMessage: string = '';

  increment(payload: number) {
    this.$store.commit('INCREMENT', payload); // 不再使用 MutationTypes
  }

  decrement(payload: number) {
    this.$store.commit('DECREMENT', payload); // 不再使用 MutationTypes
  }

  updateMessageAsync() {
    this.$store.dispatch(ActionTypes.UPDATE_MESSAGE_ASYNC, this.newMessage);
  }

  get count(): number {
    return (this.$store.state as RootState).count;
  }

  get message(): string {
    return (this.$store.state as RootState).message;
  }

  get userName(): string {
    return (this.$store.getters as RootGetters).userName;
  }

  incrementAsync: ActionsInterface[ActionTypes.INCREMENT_ASYNC];
  fetchUser: ActionsInterface[ActionTypes.FETCH_USER];
}
</script>

这里使用了 mapStatemapGettersmapActions 来简化代码。需要注意的是,在使用 this.$store.statethis.$store.getters 时,需要进行类型断言,告诉 TypeScript 它们的类型。另外, incrementAsyncfetchUser 的类型使用了接口定义的方式,保证了类型安全。

Vuex 类型安全总结

| 概念 | 类型声明方式

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注