官网文档有一部分对typescript的支持的说明
next.vuex.vuejs.org/guide/types…
首先,先尝试 state,结构如下:
- 一个全局的state,getters
- 两个模块的steae, getters, 都是开启命名空间
const user = {
namespaced: true,
state: {
flag: true
},
getters: {
isTrue (state: userState): boolean {
return state.loading
}
}
}
const foo = {
namespaced: true,
state: {
count: 1
},
getters: {
tenfold (state: fooState): number {
return state.count * 10
}
}
}
const store = createStore({
state: {
age: 17
},
getters: {
bar () => 1000000
},
modules:{
foo,
user
}
})
根据官方文档上建议进行了类型设置。
import { createStore, Store } from 'vuex'
// 配置选项中的接口
interface State {
age: number
}
// 这个key需要在组件中引入 store时使用, 没有会报错 --> useStore(key)
export const key: InjectionKey<Store<State>> = Symbol()
const store = createStore<State>({
...
})
下面是官方的 d.ts 文件中的 Store, 可以看出泛型只用传自己定义的 State 接口。
写到这差不多了,接下来在组件中引入吧!
state 的坑
好吧,代码提示只有全局的 state
属性有提示,那我们怎么读 modules 中的 state
属性呢。
store.state 中模块的 state 直接存在了对应模块名属性中,那就来引吧。
好家伙,属性不存在。但是我们都知道 typescript 只是做类型推断的,飘红不会影响程序的成功运行。如果想解决飘红的问题,就要按照 typescript 的规矩,进行类型的声明。同时,类型的声明还有一个好处,就是会有代码提示。
那我们就来声明一下吧。
interface IUserState {
flag: boolean
}
// 一个个写有点慢,而且属性多了很繁琐,这时候我们可以用 ReturnType
// 先把 state 提取出来,后续有需要也可以分开单独文件管理
const userState = () => {
return {
...
}
}
export userState
export type UserState = ReturnType<userState>
// 其他模块同理
声明完之后,只需要加入到 key
里面就可以了,因为 key
是注册 state
的地方
// 我有两个模块,把他们合并成一个 Modules 一起添加
type Modules = {
user: UserState;
foo: FooState;
}
export const key: InjectionKey<Store<State & Modules>> = Symbol()
成功提示,也不飘红。
接下来体验 getters (的“坑”)
getters的问题也是同样的,代码不提示,但是没有飘红,不过引用的方法不是 store.getters.xxx
,这是因为启用了命名空间,所以属性名就发生了改变。
setup() {
const store = useStore(key)
console.log(store.getters['user/isTrue']) // true
}
如果不需要代码提示,那么就到此结束了。但是...我们开始进行类型声明吧。 目的是提取出类似的结构。
type Getters = {
"user/isTrue": boolean;
"foo/tenfold": number;
}
// 这是我们的模块对象 modules: { foo, user }
import { modules } from './modules'
// 取出各个模块里面的 getters 属性
type GetGetter<M> = M extends { getters: infer G } ? G : unknown
type GetGetters<Ms> = {
[K in keyof Ms]: GetGetter<Ms[K]>
}
type getters = GetGetters<typeof modules>
// 提取成 ['user/isTrue', 'foo/tenfold']
type addPrefix<K, N> = `${K & string}/${N & string}`
type MergeKey<Getters> = {
[K in keyof Getters]: addPrefix<K, keyof Getters[K]>
}[keyof Getters]
// 提取成
// type ModuleGetters = {
// "user/isTrue": (state: {
// flag: boolean;
// }) => boolean;
// "foo/tenfold": (state: {
// count: number;
// }) => number;
// }
type GetFn<T, A, B> = T[A & keyof T][B & keyof T[A & keyof T]] // 这里只能一级一级的传下去
type GetFinalKey<T> = {
[K in MergeKey<T>]: K extends `${infer A}/${infer B}` ? GetFn<T, A, B> : unknown
}
type ModuleGetters = GetFinalKey<getters>
// 最后实现
// type Getters = {
// "user/isTrue": boolean;
// "foo/tenfold": number;
// }
type Getters = {
[K in keyof ModuleGetters]: ReturnType<ModuleGetters[K]>
}
export { Getters }
泛型写起来挺像写函数的, 类型声明有了,接下来找个地方添加进去吧。但是如同前面 Store
的图所显示, getters
是一个 any
, 我们无法靠泛型去改变他的类型,意味着我们不能像刚刚 state
一样处理。
我们只好把值取出来自己封装了,另外提一句,官方文档上的建议本来就有一个封装,因为考虑到如果在不同组件用 useStore(key)
的时候,都要引一次 key
很麻烦。那正好我们就把他们放在一起吧。
// index.ts
import { createStore, Store, useStore as baseUseStore } from 'vuex'
import { Getters } from '@/store/utils'
export const key: InjectionKey<Store<State & Modules>> = Symbol()
// 采用类型合并的形式,把全局和自己定义的modules合并起来
type RootGetters = {
bar: number
} & Getters
export function useStore () {
// 把需要用到的都拿出来
const { state, getters, commit, dispatch } = baseUseStore(key)
return {
state,
getters: getters as RootGetters, // 写法也许不够优雅
commit,
dispatch
}
}
我们来看看效果吧!
记得引用自己导出的 useStore
但是用官方提供的 mapState, mapGetter 就不会这么麻烦,ts代码都不需要写。
但是,还是有没法解决的问题
computed: {
...mapState({
myAge: 'age',
foo: 'foo', // 模块的 state 不用函数的话只能拿到最外层,但是官方文档的函数写法又显示没匹配的重载。
}),
...mapGetters({
isTrue: 'user/isTrue'
})
}
另外,官方也提供了 this.$store
的配置方法,这个需要自己配置,不过不太好配,有 useStore
还废这么大劲干啥^_^!
actions 和 mucations
dispatch
和commit
都会在 useStore
中返回,可以解构出来。
使用方法和vuex3差不多,
有命名空间就 dispatch('模块名字/方法名')
, commit
同理
总结
总的来说,ts支持的是比较好的,但对ts新人确实有点奇怪的飘红,但是需要写ts的地方基本上都是输入输出的类型判断。
我只是个新手,第一次写文章,有写得不对的地方,还请指出。