vuex4 中 typescript 的使用,以及 store 的一些坑

6,519 阅读4分钟

官网文档有一部分对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 接口。

Store.png

写到这差不多了,接下来在组件中引入吧!

state 的坑

storeState.png

好吧,代码提示只有全局的 state 属性有提示,那我们怎么读 modules 中的 state 属性呢。

stateProxy.png

store.state 中模块的 state 直接存在了对应模块名属性中,那就来引吧。

errorState.png

好家伙,属性不存在。但是我们都知道 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()

成功提示,也不飘红。

successState.png

接下来体验 getters (的“坑”)

getters的问题也是同样的,代码不提示,但是没有飘红,不过引用的方法不是 store.getters.xxx ,这是因为启用了命名空间,所以属性名就发生了改变。

setup() {
  const store = useStore(key)
  console.log(store.getters['user/isTrue']) // true
}

getterProxy.png

如果不需要代码提示,那么就到此结束了。但是...我们开始进行类型声明吧。 目的是提取出类似的结构。

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 一样处理。

Store.png

我们只好把值取出来自己封装了,另外提一句,官方文档上的建议本来就有一个封装,因为考虑到如果在不同组件用 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

successGetters.png

但是用官方提供的 mapState, mapGetter 就不会这么麻烦,ts代码都不需要写。

但是,还是有没法解决的问题

computed: {
  ...mapState({
    myAge: 'age',
    foo: 'foo', // 模块的 state 不用函数的话只能拿到最外层,但是官方文档的函数写法又显示没匹配的重载。
  }),
  ...mapGetters({
    isTrue: 'user/isTrue'
  })
}

另外,官方也提供了 this.$store 的配置方法,这个需要自己配置,不过不太好配,有 useStore 还废这么大劲干啥^_^!

actions 和 mucations

dispatchcommit 都会在 useStore 中返回,可以解构出来。 使用方法和vuex3差不多, 有命名空间就 dispatch('模块名字/方法名')commit 同理

总结

总的来说,ts支持的是比较好的,但对ts新人确实有点奇怪的飘红,但是需要写ts的地方基本上都是输入输出的类型判断。

我只是个新手,第一次写文章,有写得不对的地方,还请指出。