preface
Before TypeScript 4.1, we had no choice but to use any for functions like DVA and vuex that needed to be written to the namespace when triggered.
dispatch({
type: 'users/getUser'.payload: '... '.// any
})
Copy the code
This led to a breakdown in what should have been a good TS type derivation in the project, and there were solutions in the community that were implemented through more complex types and function encapsulation, which were very different from the official writing.
Fortunately, TypeScript 4.1 brings with it the Template Literal Types feature, which allows us to concatenate Types with strings, making type derivation of such functions a reality.
This article will take you one by one to explain the specific derivation of the process, I hope that after reading there will be a harvest.
Meanwhile, the final implementation of this article has been released with the NPM package dvA-Type, which can be installed and used directly in the project.
Dva basic use
Before we write the code, let’s review the basic use of DVA so we know what we’re ultimately trying to implement.
The Model definition
Dva declares the state of each module by defining model, in which reducers is the reducers of Redux, effects is the place used to perform asynchronous operations. In Effect, the state will eventually be updated to state through reducers.
cosnt model = {
state: {},
effects: {
getList(){}}reducers: {
merge(){}}}Copy the code
The basic use
Use the same method as redux
- use
connect
Higher order functions oruseSelector
In order to getstate
- use
connect
oruseDispatch
getdispatch
function
connect(state= > ({
userInfo: state.users.info,
}))
// Type fault
dispatch({
type: 'users/getUser'.payload: false,})Copy the code
The type fault mainly lies in the fact that the action type at dispatch cannot be derived, while the type hint of state is fine. The failure of action mainly lies in the fact that the parameter type requires a concatenated namespace.
So what we’re dealing with is type hints and derivations after concatenating namespaces, which after the advent of the Template Literal Types feature makes the solution incredibly simple and natural.
dva-type
Before we start parsing the dVA-type source code, let’s take a look at how it is used
Dva -type use
-
Define a single Model type (note that Model and Effect are not imported from DVA)
import { Effect, Model } from 'dva-type' interface ListModel extends Model { state: { list: any[]}effects: { // Defines the effect passed payload type getList: Effect<number> // Effect does not need payload getInfo: Effect } } Copy the code
-
Define a collection of all models in the project (using Type instead of interface)
// Use type to define models to collect all models in the project type Models = { list: ListModel info: InfoModel // ... } Copy the code
-
Pass Models to ResolverModels to get the types of state and actions
import { ResolverModels } from 'dva-type' type State = ResolverModels<Models>['state'] type Actions = ResolverModels<Models>['actions'] Copy the code
-
use
// hooks useSelector<State>() const dispatch = useDispatch<(action: Actions) = > any> ()// class const mapStateToProps = (state: State) = > {} interface Props { dispatch: (action: Actions) = > any } Copy the code
Dva-type source code parsing
As you can see from the above use, all the secrets lie in the type ResolverModels, and let’s take a look at its implementation
interface ResolverModels<T extends Record<string, Model>> {
state: ResolverState<T> & Loading<T>
actions: ResolverReducers<T> | ResolverEffects<T>
}
Copy the code
To extract the State
Parsing state is simple, using keyof to iterate over the model’s state definition.
type ResolverState<T extends Record<string, Model>> = UnionToIntersection<
{
[k in keyof T]: T[k]['state']} >Copy the code
So that’s the basic operation, so let’s go through what’s going on here
T
We passed it inModels
The type definition[k in keyof T]
It’s kind of traversalT
The key:list
,info
T[k]['state']
Is equivalent to:T [' list '] [' state ']
,T [' info '] [' state ']
This deduces the type of state, but the derived type is a union type, and we need to cast it to a cross type to do the correct type hint.
Union type conversions cross types
Turning unions into crossovers is dark magic found online:
type UnionToIntersection<U> =
(U extends any ? (k: U) = > void : never)
extends (k: infer I) => void
? I
: never
Copy the code
I don’t really understand the underlying mechanism, but let’s see what it does:
U extends any ? (k: U) => void : never
extends any
The condition is alwaystrue
, so here is the type passed inU
Changed to function type:(k: U) => void
extends (k: infer I) => void
- The first step is we changed the type to
(k: U) => void
So here’s theextends
I’m sure that would be the casetrue
。 - Pay attention to
infer I
, this will typeU
The inference is redone, and this is where the union type becomes the crossover type.
- The first step is we changed the type to
? I : never
- It is clear from steps 1 and 2 that the ternary expression here will always return
I
。 - At this point, the union type is converted to a crossover type.
- It is clear from steps 1 and 2 that the ternary expression here will always return
Extract the Actions
The Effect type provided by dVA cannot be passed into the type definition of payload, so we need to encapsulate an Effect:
type Effect<P = undefined> = (
action: { type: any; payload? : P }, effect: EffectsCommandMap) = > void
Copy the code
parsingeffects
type
type ResolverEffects<T extends Record<string, Model>> = ValueType<
{
[t in keyof T]: ValueType<
{
[k in keyof T[t]['effects']]: T[t]['effects'][k] extends (
action: { type: any; payload? : infer A },effect: EffectsCommandMap
) => void
? A extends undefined
? {
type: `${t}/${k}`
[k: string] :any}, {type: `${t}/${k}`
payload: A
[k: string] :any
}
: never} > >}Copy the code
Code a big pile, according to the process to go again:
-
T is still the Models type passed in
-
[t in keyof t] = state
-
[k in keyof T[T][‘effects’]] Models [‘ list ‘] [‘ effects’] [‘ getList ‘], Models [‘ info ‘] [‘ effects’] [‘ getInfo ‘]
-
T[t]['effects'][k] extends (action: { type: any; payload? : infer A },effect: EffectsCommandMap) => void
-
The function type after extends is the same as the Effect type we defined
-
Note that extends… payload? : infer A… And this is extracting the type of payload
-
-
A extends undefined. This step determines whether an effect needs to be passed in payload. If it does not, it does not need to be reflected in the type
-
{ type: `${t}/${k}` payload: A } Copy the code
payload: A
So this is just assigning the derived type back- type:
{k}, among themt
Represents the namespace,k
According to theeffect
The name of the:type: 'list/getList'
-
At this point, the type has been derived, but the format is not what we want:
{ list: { getList: { type: 'list/getList'.payload: number}}}Copy the code
-
We only want the innermost {type:.. payload .. T[keyof T] = T[keyof T] = T[keyof T]
-
Simply encapsulating this is what the outermost ValueType does.
The types of effects have been extracted, and reducers have done the same, so I won’t go into the details.
Dva – loading types
In DVa-loading, loading variables can be provided according to effects. After analyzing effects, the loading variable prompt is also natural
interface Loading<T extends Record<string, Model>> {
loading: {
global: boolean
models: {
[k in keyof T]: boolean
}
effects: {
[k in ResolverEffects<T>['type']] :boolean}}}Copy the code
End
OK, thanks for watching this and I hope it will improve you,