Vuex TypeScript 支持实战:为 $store 属性与 useStore 函数提供完整类型
【免费下载链接】vuex🗃️ Centralized State Management for Vue.js.项目地址: https://gitcode.com/gh_mirrors/vu/vuex
本文围绕 Vuex 官方文档中的 TypeScript 支持指南展开,系统讲解如何在 TypeScript 项目中为 Vuex 4 的三个关键点提供类型:直接编写带类型的 Store 定义、为选项式组件中的this.$store属性声明模块扩展、以及为组合式 API 的useStore函数返回带类型的 Store。读完本文,你将掌握从InjectionKey定义到app.use注入的完整类型链路,并结合 Vuex 源码(src/injectKey.js、src/store.js)理解这些类型声明背后的运行时机制。
Vuex 自带类型定义,无需额外 TypeScript 配置
Vuex 以库的形式内置了完整的类型声明(typings),因此你可以直接用 TypeScript 编写 Store 定义,不需要任何针对 Vuex 的特殊 TypeScript 配置。项目本身的类型入口指向 types/index.d.ts,package.json 中通过"typings": "types/index.d.ts"声明,并将types/index.d.ts、types/helpers.d.ts、types/logger.d.ts、types/vue.d.ts一并发布(files字段)。对使用者而言,只需按照 Vue 3 官方文档中「基本的 TypeScript 配置」来配置自己的项目即可。
在类型层面,Vuex 暴露的核心导出包括:
Store<S>类与createStore<S>(options: StoreOptions<S>): Store<S>工厂函数,其中泛型S即 Store 的 state 类型;useStore<S = any>(injectKey?: InjectionKey<Store<S>> | string): Store<S>组合式函数;StoreOptions<S>、GetterTree、ActionTree、MutationTree、Module等用于描述完整 Store 结构的接口,见 types/index.d.ts。
也就是说,只要你在createStore/new Store时给出 state 的接口定义,store.state就会获得精确类型(readonly state: S),dispatch、commit、subscribe、registerModule等 API 也都有对应的签名。
为 Vue 组件中的$store属性提供类型
虽然 Vuex 在运行时会自动把 Store 挂载到组件实例上——从 src/store.js 的install方法可以看到:
install (app, injectKey) { app.provide(injectKey || storeKey, this) app.config.globalProperties.$store = this // ... }但 Vuex 并没有开箱即用地为this.$store提供类型。用 TypeScript 编写 Vue 组件时,你必须自己声明模块扩展(module augmentation)。
具体做法:在项目的文件夹中新增一个声明文件(例如vuex.d.ts),对 Vue 的ComponentCustomProperties接口进行自定义类型声明:
// vuex.d.ts import { Store } from 'vuex' declare module '@vue/runtime-core' { // 声明你的 store state interface State { count: number } // 为 `this.$store` 提供类型 interface ComponentCustomProperties { $store: Store<State> } }这里有两个值得注意的实现细节:
- 为什么扩展目标是
@vue/runtime-core而不是vue。Vue 3 的运行时核心类型(包括ComponentCustomProperties、ComponentCustomOptions等可混入接口)都定义在@vue/runtime-core包中,vue包只是再导出它们。Vuex 自身的类型文件 types/vue.d.ts 正是对同一个模块做扩展,但它只声明了ComponentCustomOptions中的store?: Store<any>(用于createApp(Component, { store })这种传参写法),并没有覆盖$store,所以后者仍需用户自己声明。 State接口的复用。上面声明在@vue/runtime-core模块内的interface State只是该示例文档的写法;实际项目中通常把State接口单独定义并从vuex.d.ts中导入,避免声明合并带来的意外。
声明生效后,在选项式组件中this.$store.state.count即可被识别为number,this.$store.commit/this.$store.dispatch也会走 types/index.d.ts 中Dispatch/Commit接口的重载签名(同时支持'type', payload与{ type, payload }两种调用风格)。
为useStore组合式函数提供类型
用组合式 API 编写 Vue 组件时,你通常希望useStore返回带类型的 Store。为了让useStore正确返回带类型的 Store,需要完成三步:
- 定义带类型的
InjectionKey; - 向 Vue App 安装 Store 时传入这个带类型的
InjectionKey; - 把这个带类型的
InjectionKey传给useStore方法。
下面逐步说明。
第一步:定义带类型的 InjectionKey 并创建 Store
// store.ts import { InjectionKey } from 'vue' import { createStore, Store } from 'vuex' // 定义 store state 的类型 export interface State { count: number } // 定义注入 key export const key: InjectionKey<Store<State>> = Symbol() export const store = createStore<State>({ state: { count: 0 } })第二步:安装 Store 时传入 InjectionKey
// main.ts import { createApp } from 'vue' import { store, key } from './store' const app = createApp({ ... }) // 传入注入 key app.use(store, key) app.mount('#app')第三步:在组件中通过 key 获取带类型的 Store
// 在 Vue 组件中 import { useStore } from 'vuex' import { key } from './store' export default { setup () { const store = useStore(key) store.state.count // 类型是 number } }源码视角:为什么 InjectionKey 是核心
在内部实现上,Vuex 使用 Vue 的 Provide/Inject 机制把 Store 安装到 Vue App 实例上,这正是InjectionKey能承担类型传递职责的原因。从源码可以完整还原这条链路:
- src/injectKey.js 定义了默认 key 与
useStore:
export const storeKey = 'store' export function useStore (key = null) { return inject(key !== null ? key : storeKey) }即:传入 key 时用你传入的 key 去inject,否则回退到字符串'store'。
src/store.js 的
install方法与之一一对应:app.provide(injectKey || storeKey, this)—— 你通过app.use(store, key)传入的第二个参数就是injectKey,Provider 端和 Inject 端必须使用同一个 key,类型才能沿 Provide/Inject 通道精确传递。类型声明与之一致:types/index.d.ts 中
Store.install的签名为install(app: App, injectKey?: InjectionKey<Store<any>> | string): void,而useStore的签名是:
export function useStore<S = any>(injectKey?: InjectionKey<Store<S>> | string): Store<S>;注意injectKey允许传入string,这是因为运行时storeKey本身是字符串'store'。仓库自带的类型测试 types/test/index.ts 中的UseStoreFunction命名空间验证了全部四种调用形态:传入InjectionKey<string>形式的 key、传入字符串'store'、直接传泛型useStore<State>()(此时返回Store<State>)、以及不传任何参数的useStore()(泛型默认any,返回未类型化的 Store)——这也解释了为什么文档强调:想要类型,就必须把带类型的InjectionKey从定义、安装到使用一路传下去。
简化useStore的用法
每次使用useStore时都要导入InjectionKey并把它传给useStore,很快就会变成重复劳动。更简洁的做法是定义一个自己的组合函数来封装这个 key:
// store.ts import { InjectionKey } from 'vue' import { createStore, useStore as baseUseStore, Store } from 'vuex' export interface State { count: number } export const key: InjectionKey<Store<State>> = Symbol() export const store = createStore<State>({ state: { count: 0 } }) // 定义你自己的 `useStore` 组合函数 export function useStore () { return baseUseStore(key) }这样,组件侧只需导入自己的组合函数,就不需要再提供注入 key 及其类型即可拿到带类型的 Store:
// 在 Vue 组件中 import { useStore } from './store' export default { setup () { const store = useStore() store.state.count // 类型是 number } }由于baseUseStore(key)中key的类型是InjectionKey<Store<State>>,baseUseStore的泛型S会被推断为State,返回类型自然就是Store<State>。这个模式相当于把「三步走」中第 1、2 步固化到 Store 模块内,第 3 步的调用成本降为零。
类型正确性的验证方式
Vuex 仓库自身通过tsc对上述类型定义做回归验证:package.json 的test:types脚本执行tsc -p types/test,编译 types/test/index.ts 与 types/test/tsconfig.json(开启了"strict": true与"noEmit": true)。该测试文件覆盖了三条与本文直接相关的链路:
new Vuex.Store<...>各 API(dispatch/commit/watch/subscribe/subscribeAction/replaceState/registerModule/hotUpdate)的调用形态,见 types/test/index.ts 与 types/test/index.ts;useStore的 key 字符串、InjectionKey、泛型直传三种形态,见 types/test/index.ts;ActionContext<S, R>在模块(含 namespaced 模块)中state/rootState/getters/rootGetters的推断,见 types/test/index.ts。
你可以在自己的项目中参照这套思路:先用tsc --noEmit检查 Store 定义,再在组件侧验证this.$store与useStore返回值的类型是否符合预期。
小结
| 场景 | 需要的类型工作 | 关键机制 |
|---|---|---|
| 用 TS 编写 Store 定义 | 无额外配置,直接用createStore<State>/new Store<State> | 泛型S贯穿StoreOptions<S> |
选项式组件访问this.$store | 在vuex.d.ts中扩展@vue/runtime-core的ComponentCustomProperties | 运行时由app.config.globalProperties.$store挂载(见 src/store.js) |
组合式 API 使用useStore | 定义InjectionKey<Store<State>>,app.use(store, key),useStore(key) | Provide/Inject 通道(见 src/injectKey.js) |
| 简化组合式 API 用法 | 在 Store 模块内封装一个闭包持有 key 的useStore() | 泛型推断自InjectionKey类型 |
需要留意的前提:本仓库对应 Vuex 4.x(当前版本 4.1.0),peerDependencies要求vue ^3.2.0,因此上述app.use(store, key)、InjectionKey等写法均基于 Vue 3 应用;若你的项目仍在使用 Vue 2 + Vuex 3,类型方案与注入机制均不相同,本文内容不适用。
【免费下载链接】vuex🗃️ Centralized State Management for Vue.js.项目地址: https://gitcode.com/gh_mirrors/vu/vuex
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考