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
- Can prompt
getters
/rootGetters
All available properties and their types in - Can prompt
dispatch
All availabletype
And the correspondingpayload
The type of - Can prompt
commit
All availabletype
And the correspondingpayload
The type of - Above effect no matter in
module
, a global (this.$store
) oruseStore
Are 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
- 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
- The writing environment of the code in this paper is
TypeScript 4.3.5
Later 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
Exclude
Is used toK
From the union typekeyof T
Rule outReturnType
The return type used to return the function, which is obtained heregetters
Return 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
GettersTypes
Designed as ajs
Object rather thanenum
First, considering that this is not an enumeration scenario, and second, for claritygetter
的key
, as well askey
Corresponding method and its return value if usedenum
Will be lost due to type ambiguitykey
The connection between the corresponding method and its return value is lostGettersTypes
You must use theas const
Modify the details visible to me beforeOne of the articles I wroteThere are aboutConst Assertions
A description of theExtract<key, string>
The way I wrote it is becauseTypeScript
One that existsfeature[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
Mutions
The 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]