各位观众老爷,大家好!今天咱们来聊聊 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
返回 number
,userName
返回 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 函数接收 state
和 payload
作为参数,state
的类型是 RootState
,payload
的类型根据实际情况定义。
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
包含 state
、commit
、dispatch
等属性。为了类型安全,我们需要对 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>
这里使用了 mapState
、mapGetters
和 mapActions
来简化代码。需要注意的是,在使用 this.$store.state
和 this.$store.getters
时,需要进行类型断言,告诉 TypeScript 它们的类型。另外, incrementAsync
和 fetchUser
的类型使用了接口定义的方式,保证了类型安全。
Vuex 类型安全总结
| 概念 | 类型声明方式