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

  • useconnectHigher order functions oruseSelectorIn order to getstate
  • useconnectoruseDispatchgetdispatchfunction
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

  1. 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
  2. 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
  3. 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
  4. 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

  1. TWe passed it inModelsThe type definition
  2. [k in keyof T]It’s kind of traversalTThe key:list,info
  3. 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:

  1. U extends any ? (k: U) => void : never
    • extends anyThe condition is alwaystrue, so here is the type passed inUChanged to function type:(k: U) => void
  2. extends (k: infer I) => void
    • The first step is we changed the type to(k: U) => voidSo here’s theextendsI’m sure that would be the casetrue
    • Pay attention toinfer I, this will typeUThe inference is redone, and this is where the union type becomes the crossover type.
  3. ? I : never
    • It is clear from steps 1 and 2 that the ternary expression here will always returnI
    • At this point, the union type is converted to a crossover type.

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
parsingeffectstype
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:

  1. T is still the Models type passed in

  2. [t in keyof t] = state

  3. [k in keyof T[T][‘effects’]] Models [‘ list ‘] [‘ effects’] [‘ getList ‘], Models [‘ info ‘] [‘ effects’] [‘ getInfo ‘]

  4. 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

  5. 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

  6. {
      type: `${t}/${k}`
      payload: A
    }
    Copy the code
    1. payload: ASo this is just assigning the derived type back
    2. type:
      t / {t}/
      {k}, among themtRepresents the namespace,kAccording to theeffectThe name of the:type: 'list/getList'
  7. 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
  8. We only want the innermost {type:.. payload .. T[keyof T] = T[keyof T] = T[keyof T]

  9. 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,