Vue 3.x has been around for a long time, but I haven’t been paying much attention to it since I work with React on the tech stack. Recently, I feel Vue 3.x has almost stabilized, so I took some time to learn about it. I have to say that Vue 3.x supports TypeScript quite well. With the Volar plug-in, the code is written smoothly

I also learned about vue-Router, vuex and other related eco-libraries, and wrote a Demo to test them. Since I already have good support for TypeScript, and I also prefer ts, I try to avoid writing any type. When vuex was introduced, I took a look at its support for TS and realized that it was too simple. Having recently learned TypeScript gymnastics, I decided to wield a hammer on vuex

For intuitive purposes, the shopping cart example placed on Github by Vuex is transformed, mainly taking cart module as an example, similar to products module

The effect that this article will achieve

  1. Can promptgetters/rootGettersAll available properties and their types in
  2. Can promptdispatchAll availabletypeAnd the correspondingpayloadThe type of
  3. Can promptcommitAll availabletypeAnd the correspondingpayloadThe type of
  4. Above effect no matter inmodule, a global (this.$store) oruseStoreAre effective

As a new player in TypeScript, I encountered a lot of problems while writing this article, but I’ve solved them all, and I’ll mention some that I found useful as a reference

This article is a bit of a hands-on experience, even if you don’t know much about TypeScript type programming

  1. The complete example code of this article has been uploaded to Github and can be directly used as a sample code for your own projects
  2. The writing environment of the code in this paper isTypeScript 4.3.5Later than this version may not compile correctly due to the use of advanced features

Transform Getters

Fill in the state type according to the business logic of the code

// store/modules/cart.ts
export type IProductItem = { id: number.title: string.inventory: number; quantity: number }
export type TState = {
  items: ArrayThe < {id: number.quantity: number} >. checkoutStatus:null | 'successful' | 'failed'
}

const state: () = > TState = () = > ({
  items: [].checkoutStatus: null
})
Copy the code

Then start adding types to getters. First go to vuex’s Types folder to see if there are any defined types. Find GetterTree

import { GetterTree } from 'vuex'

const getters: GetterTree<TState, TRootState> = {
  // ...
}
Copy the code

TState is the state type of the current module, and TRootState is the global state type. For example, in this example, there are two modules, CART and Products, so the TRootState type is:

// store/index.ts
import { TState as TCartState } from '@/store/modules/cart'
import { TState as TProductState } from '@/store/modules/products'

export type TRootState = {
  cart: TCartState;
  products: TProductState;
}
Copy the code

The TRootState attributes cart and Products are the namespace names of the CART module and products module, respectively, which will be encountered frequently in the future, so it is best to define them uniformly

// store/modules/cart.ts
export const moduleName = 'cart'
// store/modules/products.ts
export const moduleName = 'products'

// store/index.ts
import { moduleName as cartModuleName } from '@/store/modules/cart'
import { moduleName as productsModuleName } from '@/store/modules/products'

export type TRootState = {
  [cartModuleName]: TCartState;
  [productsModuleName]: TProductState;
}
Copy the code

After adding GetterTree, the type of the first parameter state and the third parameter rootState of the methods under getters (such as cartProducts) can be automatically deduced

However, the second getter and the fourth parameter rootGetters are still of type any, because the default type of GetterTree for these two parameters is any, so we need to change them manually

// store/modules/cart.ts
import { TRootState, TRootGetters } from '@/store/index'
import { GettersReturnType } from '@/store/type'

const GettersTypes = {
  cartProducts: 'cartProducts'.cartTotalPrice: 'cartTotalPrice'
} as const
type VGettersTypes = (typeof GettersTypes)[keyof typeof GettersTypes]

export type TGetters = {
  readonly [key in VGettersTypes]: (
    state: TState, getters: GettersReturnType<TGetters, key>, rootState: TRootState, rootGetters: TRootGetters
  ) = > key extends typeof GettersTypes.cartProducts ? ArrayThe < {title: string;
    price: number;
    quantity: number; } > :number
}

// getters
const getters: GetterTree<TState, TRootState> & TGetters = {
  [GettersTypes.cartProducts]: (state, getters, rootState, rootGetters) = > {
    // ...}}Copy the code

New GettersTypes object to explicitly enumerate the key of getters and new TGetters type to override GetterTree

GettersReturnType

is used to get the return type of getters
,>

// store/type.ts
export type GettersReturnType<T, K extends keyof T> = {
  [key in Exclude<keyof T, K>]: T[key] extends(... args:any) = >any ? ReturnType<T[key]> : never
}
Copy the code
  1. ExcludeIs used toKFrom the union typekeyof TRule out
  2. ReturnTypeThe return type used to return the function, which is obtained heregettersReturn type of

TRootGetters is the global getters, and its key is the namespace + key of module getters, and the value is still the corresponding value of module getters. Using the template string ability of TS, the namespace and the key of getters are combined. Get the key of TRootGetters

// store/type.ts
type RootGettersReturnType<T extends Record<string.any>, TModuleName extends string> = {
  readonly [key in keyof T as `${TModuleName}/${Extract<key, string>}`]: T[key] extends ((. args:any) = > any)? ReturnType<T[key]> :never
}

// store/index.ts
export type TRootGetters = RootGettersReturnType<TCartGetters, typeof cartModuleName>
  & RootGettersReturnType<TProductsGetters, typeof productsModuleName>
Copy the code
  1. GettersTypesDesigned as ajsObject rather thanenumFirst, considering that this is not an enumeration scenario, and second, for claritygetterkey, as well askeyCorresponding method and its return value if usedenumWill be lost due to type ambiguitykeyThe connection between the corresponding method and its return value is lost
  2. GettersTypesYou must use theas constModify the details visible to me beforeOne of the articles I wroteThere are aboutConst AssertionsA description of the
  3. Extract<key, string>The way I wrote it is becauseTypeScriptOne that existsfeature
  4. [key in keyof T as ${TModuleName}/${Extract<key, string>}]Can be seen inKey Remapping in Mapped Types

After the above modification, you can now click on all the properties of getters and rootGetters in the editor

Transform Mutions

Let’s go to Vuex and see if there’s a defined type, and there’s an ActionTree, so let’s put that in

// store/modules/cart.ts
import { MutationTree } from 'vuex'
const mutations: MutationTree<TState> = {
  // ...
}
Copy the code

Since the key type of the MutationTree is string, it cannot be directly pointed out, so it needs to manually write another type to overwrite. In order to clarify the mutation key, it is the same as getters. Start by defining the corresponding object variable MutationTypes

// store/modules/cart.ts
import { MutationTree } from 'vuex'

const MutationTypes = {
  pushProductToCart: 'pushProductToCart'.incrementItemQuantity: 'incrementItemQuantity'.setCartItems: 'setCartItems'.setCheckoutStatus: 'setCheckoutStatus'
} as const
type TMutations = {
  [MutationTypes.pushProductToCart]<T extends { id: number }>(state: TState, payload: T): void;
  [MutationTypes.incrementItemQuantity]<T extends { id: number }>(state: TState, payload: T): void;
  [MutationTypes.setCartItems]<T extends { items: TState["items"] }>(state: TState, payload: T): void;
  [MutationTypes.setCheckoutStatus](state: TState, payload: TState["checkoutStatus") :void;
}
// mutations
const mutations: MutationTree<TState> & TMutations = {
  [MutationTypes.pushProductToCart] (state, { id }) {
    state.items.push({ id, quantity: 1 })
  },
  [MutationTypes.incrementItemQuantity] (state, { id }) {
    const cartItem = state.items.find(item= > item.id === id)
    cartItem && cartItem.quantity++
  },
  [MutationTypes.setCartItems] (state, { items }) {
    state.items = items
  },
  [MutationTypes.setCheckoutStatus] (state, status) {
    state.checkoutStatus = status
  }
}
Copy the code

MutionsThe transformation is relatively simple, and that’s it

Transforming the Actions

Start by defining the corresponding enumeration object variable

// store/modules/cart.ts
export const ActionTypes = {
  checkout: 'checkout'.addProductToCart: 'addProductToCart',}as const
Copy the code

Then find the pre-defined type and add it

// store/modules/cart.ts
const actions: ActionTree<TState, TRootState> {
  // ...
}
Copy the code

Finally, write the type TActions as usual to override

// store/modules/cart.ts
type TActions = {
  [ActionTypes.checkout](context: any.payload: TState["items") :Promise<void>
  [ActionTypes.addProductToCart](context: any.payload: IProductItem): void
}
const actions: ActionTree<TState, TRootState> & TActions {
  // ...
}
Copy the code

Context is also of type any, which is definitely not possible. From the type definition given by Vuex, context is an object with five properties:

export interface ActionContext<S, R> {
  dispatch: Dispatch;
  commit: Commit;
  state: S;
  getters: any;
  rootState: R;
  rootGetters: any;
}
Copy the code

The types of state, getters, rootState and rootGetters have all been determined. As for Dispatch and commit, the key of type signature is string, so it cannot be clicked, so it needs to change these two

// store/type.ts
import { ActionContext } from 'vuex'

type TObjFn = Record<string.(. args:any) = > any>

export type TActionContext<
  TState, TRootState,
  TActions extends TObjFn, TRootActions extends Record<string, TObjFn>,
  TMutations extends TObjFn, TRootMutations extends Record<string, TObjFn>,
  TGetters extends TObjFn, TRootGetters extends Record<string.any>
> = Omit<ActionContext<TState, TRootState>, 'commit' | 'dispatch' | 'getters' | 'rootGetters'>
  & TCommit<TMutations, TRootMutations, false>
  & TDispatch<TActions, TRootActions, false>
  & {
    getters: {
      [key in keyof TGetters]: ReturnType<TGetters[key]>
    }
  }
  & { rootGetters: TRootGetters }

// store/index.ts
type TUserActionContext = TActionContext<TState, TRootState, TActions, TRootActions, TMutations, TRootMutations, TGetters, TRootGetters>
export type TActions = {
  [ActionTypes.checkout]: (context: TUserActionContext, payload: TState["items"]) = > Promise<void>
  [ActionTypes.addProductToCart]: (context: TUserActionContext, payload: IProductItem) = > void
}
Copy the code

The context is defined as TUserActionContext, and the bottom layer is TActionContext, TActionContext still uses the ability of vuex to type ActionContext, Following the support of state and rootState types, dispatch, commit, getters and rootGetters need to be manually overwritten. Therefore, Omit these types and define them separately

Since you can call dispatch and COMMIT between modules, passing a third parameter to TCommit and TDispatch identifies whether they are in the current module, another module, or the global environment, and determines whether to add a module namespace. For example, cart/checkout

Commit depends on mutation, so TCommit is passed TMutations, TRootMutations; Similarly, pass TActions and TRootActions to TDispatch

Similar to TRootState and TRootActions, TRootMutations and TRootActions are also a collection of mutaions and actions for all modules

// store/index.ts
import {
  moduleName as cartModuleName,
  TActions as TCartActions, TMutations as TCartMutations, TCartStore
} from '@/store/modules/cart'
import {
  moduleName as productsModuleName,
  TActions as TProductsActions, TMutations as TProductsMutations, TProductsStore
} from '@/store/modules/products'

export type TRootActions = {
  [cartModuleName]: TCartActions;
  [productsModuleName]: TProductsActions;
}
export type TRootMutations = {
  [cartModuleName]: TCartMutations;
  [productsModuleName]: TProductsMutations;
}
Copy the code

getters & rootGetters

{
  getters: {
    [key in keyof TGetters]: ReturnType<TGetters[key]>
  }
}
Copy the code

Getters override the original getters, which basically give the key of the explicitly defined property and the corresponding type of the key, that is, the type of the getter

// store/index.ts
export type TRootGetters = RootGettersReturnType<TCartGetters, typeof cartModuleName>
  & RootGettersReturnType<TProductsGetters, typeof productsModuleName>

// store/type.ts
export type RootGettersReturnType<T extends Record<string.any>, TModuleName extends string> = {
  readonly [key in keyof T as `${TModuleName}/${Extract<key, string>}`]: T[key] extends ((. args:any) = > any)? ReturnType<T[key]> :never
}
Copy the code

RootGetters are globally accessible getters with a namespace

After this step, you can click on all available properties on getters and rootGetters

TCommit

// store/type.ts
export type TCommit<
  TMutations extends TObjFn, TRootMutations extends Record<string, TObjFn>, UseInGlobal extends boolean
> = {
  commit<
    M = UseInGlobal extends true
      ? UnionToIntersection<FlatRootObj<TRootMutations>>
      : (UnionToIntersection<FlatRootObj<TRootMutations>> & TMutations),
    K extends keyof M = keyof M
  >(
    key: K,
    payload: Parameters<Extract<M[K], (. args:any) = > any> > [1], options? : CommitOptions ):void
}
Copy the code

Commit is a method that takes three arguments, the last of which, options, is still supplied using vuex and returns void

Commit The first parameter is the type of the commit. There are two cases: internal module (UseInGlobal is false) and global module (UseInGlobal is false). The former type does not require a namespace prefix. The latter needs to be used, so use global to distinguish the two cases for the convenience of subsequent judgment

TRootMutations is the total collection of mutations under all modules, so it needs to use FlatRootObj for flatlining

type FlatRootObj<T extends Record<string, TObjFn>> = T extends Record<infer U, TObjFn>
  ? U extends keyof T ? {
    [key in keyof T[U] as `${Extract<U, string>}/${Extract<key, string>}`]: T[U][key]
  } : never : never
Copy the code

When flatted, the result is a union type. The key of the flatted object has a namespace added, but this is a union type. We want the result to be a cross type

type UnionToIntersection<U extends TObjFn> =
  (U extends TObjFn ? (k: U) = > void : never) extends ((k: infer I) = > void)? I :never
Copy the code

UnionToIntersection used to joint types to cross types, namely, A | B | C = > A & B & C, principle of visible type inference – in – conditional – types

The functions of FlatRootObj and UnionToIntersection are explained

type A = { q: 1; w: '2' }
type B = { e: []; r: true; }
type C = { a: A; b: B; }

type D = FlatRootObj<C>
// => { "a/q": 1; "a/w": '2'; } | { "b/e": []; "b/r": true; }
type E = UnionToIntersection<D>
// => { "a/q": 1; "a/w": '2'; } & { "b/e": []; "b/r": true; }
{"a/q": 1; "a/w": '2'; "b/e": []; "b/r": true; }
Copy the code

The second parameter payload is the submitted value, which is the first parameter of the TMutations type method signature, and the Parameters built-in method can be used to extract the Parameters of the function

At this point, you can click on all available properties after commit

It can even automatically indicate the parameter type of the second payload based on the first selected key

TDispatch

export type TDispatch<
  TActions extends TObjFn, TRootActions extends Record<string, TObjFn>, UseInGlobal extends boolean,
> = {
  dispatch<
    M = UseInGlobal extends true
      ? UnionToIntersection<FlatRootObj<TRootActions>>
      : (UnionToIntersection<FlatRootObj<TRootActions>> & TActions),
    K extends keyof M = keyof M
  >(
    key: K,
    payload: Parameters<Extract<M[K], (. args:any) = > any> > [1], options? : DispatchOptions ):Promise<ReturnType<Extract<M[K], (. args:any) = > any> > >; }Copy the code

It’s similar to TCommit, except that they come from different data sources and dispatch returns a Promise

Once the above types are written, it’s nice to see the editor intelligently sign parameters to all dispatch and commit methods in its module and in other modules

Similarly, after you write the first type parameter of dispatch and commit, you can correctly give the exact type of the second parameter payload

$store

That’s not all. In addition to getting getters and state within the Module and calling Dispatch and commit, I can also use them globally, such as:

this.$store.dispatch(...)
this.$store.commit(...)
this.$store.getters.xxx
Copy the code

With that in mind, it’s easy to just map the type globally

First add the $store signature to shims-vue.d

// shims-vue.d
import { TRootStore } from '@/store/index'

declare module '@vue/runtime-core' {
  interface ComponentCustomProperties {
    $store: TRootStore
  }
}
Copy the code

The TRootStore is a collection of all module stores, similar to TRootState and TRootActions

// store/index.ts
import { TCartStore } from '@/store/modules/cart'
import { TProductsStore } from '@/store/modules/products'

export type TRootStore = TCartStore & TProductsStore
Copy the code

So how do we get TCartStore

// store/modules/cart.ts
import { TStore } from '@/store/type'
import { TRootActions, TRootMutations } from '@/store/index'

export const moduleName = 'cart'
type TModuleName = typeof moduleName

export type TCartStore = TStore<
  { [moduleName]: TState },
  TCommit<TMutations, TRootMutations, true>,
  TDispatch<TActions, TRootActions, true>,
  {
    [key in keyof TGetters as `${TModuleName}/${key}`]: ReturnType<TGetters[key]>
  }
>
Copy the code

With the help of the TStore type, it receives four parameters: TState, TCommit, TDispatch, and getters for the current Module

The last getters is handled a little extra, since the global call to getters requires a namespace, so we use the template string to concatenate the TModuleName first

Let’s look at the implementation of TStore

// store/type.ts
import { Store as VuexStore, CommitOptions, DispatchOptions } from 'vuex'

export type TStore<
  TState extends Record<string.any>,
  TCommit extends { commit(type: string, payload? :any, options? : CommitOptions |undefined) :void },
  TDispatch extends { dispatch(type: string, payload? :any, options? : DispatchOptions |undefined) :Promise<any> },
  TGetters
> = Omit<VuexStore<TState>, 'commit' | 'dispatch' | 'getters'> & TCommit & TDispatch & {
  getters: TGetters
};
Copy the code

With the help of vuex’s good types of VuexStore, commit, Dispatch and getters are customized, so we remove these three and replace them with TCommit, TDispatch and TGetters defined by ourselves

Here we do extra processing for TGetters again, because the global $store call to getters is not called directly, but via the getters property, i.e. :

this.$store.getters['cart/cartProducts']
Copy the code

Finally, useStore also needs to be assigned the same type

// store/index.ts
import { InjectionKey } from 'vue'
import { Store as VuexStore, useStore as baseUseStore } from 'vuex'

export type TRootStore = TCartStore & TProductsStore

const key: InjectionKey<VuexStore<TRootState>> = Symbol('store')

export function useStore () :TRootStore {
  return baseUseStore(key)
}
Copy the code

Then you can happily use $store globally

Including useStore

summary

I found a third-party TypeScript library vuex-typescript-interface that supports Vuex, but it’s not that supportive

Of course, vuex’s TypeScript support is far from perfect (mapState, mapActions, multilevel nested modules, etc.), but it’s certainly better than the official TypeScript support

In order to get a good type experience, the amount of type gymnastics code is certainly not less than where, even as the real business code, but this is only in the early stage of the project, as the project becomes more and more complicated, type code will definitely account for less and less, but the role can be the same as the original

For a weakly typed language like javascript, correctly pointing out all the attributes of a variable in a large, bloated, collaborative project code is what I like to call a second comment

Recruit a hire

Finally, make an advertisement

Byte Business Products – Customer growth Team is looking for outstanding front and back end students ~

Cross-end, BI, transaction realization, CRM and other commercial products have clear planning and sufficient funds, but now there is only a programmer (manual dog head).

What, you asked me whether there really is HC or whether someone brush KPI? Isn’t the in-push rush tag after a bunch of posts on the in-push system funny?

All the students who send my resume, I guarantee to personally follow up the whole process from the resume into the system to the end of the interview, feel free to ask me the progress of the interview, if you want to take action, send my resume to [email protected]