Rematch Code Deep Dive: Part 6 - The Rematch type system

This is the final article of the Rematch Code Deep Dive Series, focusing on Rematch’s type system.
This final part of the series discusses the type system behind Rematch, which was my main contribution to the Rematch team. While refactoring it, I encountered numerous problems: some were resolved, some required “unique” design decisions due to trade-offs, some were limitations of the TS language, and others remain unsolved. In this article, I will discuss these issues for further exploration.
Due to the extensive related code, not all of it is included below. Click here to view all the code.
1 Core Types
1.1 Model
In Rematch, a key concept is the Model. Before diving deep into the Rematch type system, we need to understand this concept. Here is its definition:
|
|
We are mainly concerned with state
, reducers
, and effects
. State
is straightforward, so let’s look at the latter two.
1.2 ModelReducers
|
|
In ModelReducers
, it’s important to note that the first parameter of the Reducer
function is state
.
1.3 ModelEffects and ModelEffectsCreator
Effects
support two types: plain object-defined ModelEffects
and function ModelEffectsCreator
. Let’s start with the first one:
ModelEffects
|
|
ModelEffects
and ModelReducers
are similar, but the former receives the generic type TModels
, which contains information about all Models. We use the RematchRootState
type to extract the global rootState
as the second parameter of ModelEffect
. The latter only requires the corresponding model state information. We will discuss RematchRootState
below.
Moreover, ModelEffect
binds the context’s this
to dispatch[currentModel]
, so actions can be dispatched using this[reducerName | effectName]
. However, there are issues with type inference here, which I will address in the final problem summary section.
ModelEffectsCreator
|
|
Besides the ModelEffects
method, effects
can also be defined as a function taking dispatch
as a parameter and returning ModelEffects
. This allows to dispatch actions of the current model using the context this
in effects
, as well as dispatching actions of all models using dispatch
. We will discuss RematchDispatch
later.
1.4 RematchRootState
Those familiar with Redux know it has two core parts: the action-dispatching dispatch and the global RootState. When studying Rematch Model earlier, we encountered two types, RematchRootState
and RematchDispatch
. Let’s discuss them in detail.
First, let’s look at the type definition of RematchRootState
:
|
|
Simply put, it obtains each Model’s state, combining them with modelName
as the key. Models include both user-defined models and those exported by used plugins (e.g., using the loading plugin exports the loading Model).
1.4.1 About Generics type TModels
and TExtraModels
Attentive readers may have noticed that many types above use two generic parameters, TModels
and TExtraModels
, extensively used in Rematch’s type system. TModels
is mandatory, representing user-defined Models, while TExtraModel
is optional, used if the user utilizes plugins exporting Models.
Initially, I set default values for both generic parameters as {}
, but {}
does not mean an empty type; it represents any non-empty value, so I changed it to Record<string, any>
. However, this was also type-unsafe.
Eventually, considering TModels
is mandatory (since users of Rematch will definitely define Models), I removed the default value for TModels and changed the default value for TExtraModel
to Record<string, never>
. Using Record<string, unknown>
is also type-safe, but it does not satisfy the extends Models<TModels>
constraint (since unknown
cannot be assigned to Model
), so I switched to never
.
1.5 RematchDispatch, the hybrid ReduxDispatch
Next, let’s look at RematchDispatch
:
|
|
Firstly, RematchDispatch
is the core of Rematch, with complex type inference, so I’ve simplified it here. First, Rematch’s dispatch
is a hybrid type based on Redux dispatch
. Hence the use of ReduxDispatch & ....
.
Secondly, it needs to extract corresponding actions from effects
and reducers
. Since the reducer’s first parameter is model state, TModel['state']
information is passed in.
Lastly, reducerActions and effectActions are combined using MergeExclusive
. Initially, Rematch directly used the &
operator to combine, but as each action in Rematch carries information like { isEffect: boolean }
, if a reducer and effect share the same name, a type incompatibility issue arises (since no type can simultaneously be compatible with { isEffect: true }
and { isEffect: false }
). Here’s a simple example:
|
|
In truth, even if an effect and reducer have the same name, Rematch’s code supports it. I will simply discuss the type compatibility issue here and continue the discussion with everyone later.
2 createModel
helper function
Besides the core types, I also designed a utility function createModel
in Rematch. This function doesn’t have any practical effect and is only used to perfect types, reducing the need for users to manually add them. Here is the related code:
|
|
The function parameter is empty, and the return value is also a function, whose parameter is a Model
object and returns the Model
object itself, keeping the Model
’s attribute types unchanged. The main functions are as follows:
- By defining the type of
state
, the type of the first parameter in reducers is passed through, avoiding repetitive definitions. - By passing in the
RootModel
generic parameter, the type of the first parameterdispatch
in effects is automatically inferred. - By passing in the
RootModel
generic parameter, the type of the second parameterrootState
in individual effects is automatically inferred.
Initially, ModelCreator
was like this:
|
|
Although much simpler, the above could not meet the first point as the state
type was not connected. Then I changed it to:
|
|
However, the above two approaches fail to ensure that the return type is inferred as the user-defined Model
type. In the first approach, the return type M
is directly inferred as Model<RootModel, ModelState>
. In the second approach, although the attributes are listed, the type of an individual attribute like reducers
is just ModelReducers<ModelState>
. This means that the specific individual reducer type, like SET_PLAYERS
in the above example, is not deduced, and the contextual type is lost.
That’s why I eventually adopted a fully expanded approach, which made all functionalities achievable.
Although this method works, at first glance, one might wonder why two functions were used.
The reason is that TypeScript currently does not support partial type parameter inference. This means that if a function has multiple generic parameters, when calling this function, either all parameters must be provided by the user, or they are all inferred by TS automatically.
Therefore, I devised a two-function approach: the first function allows the user to pass specific generic parameters, while the second function does not require the user to specify anything, leaving it to TS for automatic inference. Interestingly, I was not aware of “partial type parameter inference” at the beginning of the design and did not know that this “double function” design could help solve this problem. I only discovered this PR later, and someone even specifically mentioned this design in the replies.
Finally, one might ask why I didn’t just define a single function type, but instead used the ModelCreator
interface definition. This was to support function overloading in different modules. By representing the function with an interface, Module Augmentation can be utilized, which I will discuss separately later.
3 Circularly Reference
When I previously mentioned the Model
type, did you notice a detail?
|
|
Take a close look at the generic parameter TModels
of Models
, which is constrained to Models<TModels>
. It might be a bit confusing, but this was an unintended design on my part. Initially, I directly used a default parameter:
|
|
But the above approach indeed uses any
, and as someone who strictly disciplines themselves as a “TS Gymnast,” I of course want to reduce the occurrence of any
. Therefore, I planned to change it to never
or unknown
. However, I quickly encountered a problem:
If the generic parameter TModels
of Model
uses Record<string, never>
, then the type of effects
, one of the properties of Model
, would be ModelEffectsCreator<Record<string, never>>
. Since ModelEffectsCreator
is a function, its parameter would be inferred as RematchDispatch<Record<string, never>>
.
As I mentioned in a previous article, function parameter compatibility is contravariant, so here we only need to determine whether RematchDispatch<Record<string, never>>
is compatible with RematchDispatch<TModels>
(i.e., whether the latter can be assigned to the former).
Let’s continue to analyze RematchDispatch
, where we need to extract effects
information from Model
:
|
|
But since Model
at this time is never
, then effects
will also be never
, so the index effectKey
above will also be never
. As we know, never
cannot be compatible with any type other than itself, meaning any type other than itself cannot be assigned to it. Therefore, the above RematchDispatch<TModels>
cannot be assigned to RematchDispatch<Record<string, never>>
. So, in the end, it will report an error of compatibility with the index signature. Below is the error stack:
|
|
Since never
didn’t work, what about unknown
?
|
|
Clearly, using unknown
here doesn’t even satisfy the constraint of extends Models
, because unknown
definitely cannot be assigned to Model
.
Finally, because I always believed that if users are using Rematch, they would definitely define TModels
, this generic parameter actually does not need a default value. So, I removed the default value, but without a default value, extends Models
definitely wouldn’t work, so I happened to change it to extends Models<TModels>
, and it eventually became the below somewhat strange circular reference:
|
|
Although everything now works normally, I don’t actually understand this kind of writing. I only knew it could solve my current problem. To be honest, if it weren’t for writing this article, I might not have studied this place deeply, but writing forced me to understand this kind of writing. In the process of searching, I even found a good explanation.
Returning to my problem, why did I use this method? The primary reason is that I needed to create a constraint. As mentioned earlier, Model
needs to get all the models’ information, and each individual Model
acts as an attribute of Models
. Therefore, to connect types, I needed to add the generic parameter TModels
to Models
, and TModels
needs to meet the constraint, as it definitely is also a subset of Models
, representing the user-defined global models type. So, the final constraint of TModels
became extends Models<TModels>
. It might still sound a bit convoluted, so let me give an actual example:
|
|
If we substitute TModels
with RootModel
, it becomes much easier to understand. Here, I’m essentially trying to ensure that TModels
is a subset of Models
(i.e., the actual RootModel
created by the user), not Models
itself. If it’s still hard to understand, you can directly look at this answer, where the examples in the answer might be more appropriate.
The author also mentioned that TS supports polymorphic this
, so my definition here could actually bypass the cumbersome and hard-to-understand “circular” and be changed to a more elegant way:
|
|
This this
can also represent the user-defined models. It’s worth noting that this is different from directly replacing it with Models
. If we use Model<Models>
, and at this time Models
cannot represent the user-defined models, its keys are all string types, making it meaningless to pass to Model
.
Of course, I haven’t practiced the above method yet. Later, I will find time to submit a PR to continue refactoring this part of the code.
4 Module Augmentation
TS has a feature called declaration merging, which has two merging modes for third-party modules and global environments: Module Augmentation and Global Augmentation, respectively. Augmentation means “enhancement, addition,” referring to expanding the functionality (at the type level) of a module or globally.
Global Augmentation is briefly mentioned here. If in a module (a file containing import
, export
keywords), additional declarations need to be placed under declare global {}
:
|
|
If it’s already a global script file, then there’s no need to add the declare global {}
block.
Here, let’s mainly talk about Module Augmentation. Rematch uses this mode in 3 places to eliminate type incompatibility.
4.1 @rematch/core
First is declare module '@rematch/core' {}
, used in both the select and typed-state plugins. Let’s first look at the select plugin:
|
|
The select plugin adds a selectors
attribute to Model
and also exports a select
function, mounted on RematchStore
.
When explaining the createModel
helper earlier, I mentioned why ModelCreator
is defined as an interface. Since the select plugin supports defining selectors in the model, it can conveniently utilize Module Augmentation for function overloading.
The typed-state plugin is mainly for developers using Rematch purely in JS. Through the typings
configuration, it helps to identify some erroneously defined types in the development environment. It also uses Module Augmentation:
|
|
4.2 redux
@rematch/core
also uses Module Augmentation for redux
:
|
|
When previously mentioning RematchDispatch
, I talked about it being a composite type, combined with Rematch’s own dispatcher and ReduxDispatch
(& ReduxDispatch
). Below is ReduxDispatch
:
|
|
Adding the above definition and using any
can eliminate many type errors in the source code. For developers using Rematch, models information is predefined, but in the source code, it can only be expressed using the generic TModels
. There are several errors in this area, some of which I couldn’t find the reason for and just used any
to avoid them. This part could be considered a lingering issue, and interested developers are welcome to check out the source code and submit PRs if they can resolve it.
You might wonder why we need to define a separate RematchDispatch
instead of just using Module Augmentation. This is because TS requires declarations with the same name to use the same generic parameters, and RematchDispatch
needs TModels
information, which cannot be consistent with ReduxDispatch
.
4.3 react-redux
Rematch also made compatibility adjustments for the connect
method in react-redux
. Since connect
can only recognize ReduxDispatch
, it needs to be overloaded to support RematchDispatch
:
|
|
By overloading, we introduce the generic RM
, which is TModels
, so that the dispatch
parameter can be compatible, making connect
compatible as well.
It’s worth mentioning that, according to Redux’s StyleGuide, it is recommended to use hooks, i.e., useSelector
and useDispatch
, instead of connect
. This is also applicable to Rematch. The Redux team also believes that connect
’s type definitions are overly complex and difficult to use, with too many function overloads, optional parameters, and the need to merge props, etc. Interested readers can check the link for more details.
While same-named declarations require the same generic parameters, if the declaration is used for function overloading, the overloaded functions’ generic parameters can be different.
5 Problem Summary
Finally, I picked a few typical issues, some of which have been resolved and some are still unresolved. I searched a lot of material for these issues and even found some design limitations of TS, all of which are quite interesting and worth discussing.
5.1 【Resolved】dispatcher inference
The original PR can be viewed here.
I previously mentioned RematchDispatch
, which is actually a core part of Rematch and has complex type inference. For example, a reducer defined in a model has three parameters: modelState
, payload
, and meta
, but when calling dispatch, only payload
and meta
need to be passed. In an effect, there are also three parameters: payload
, rootState
, and meta
, with payload
and meta
being passed during the call.
Extracting parameters and generating new function definitions seems simple but has its challenges.
Let’s look at the extraction of reducers:
|
|
Note that RematchDispatcher
uses void
as the default generic parameter. It was only while writing this article that I discovered a bug. Because void
, apart from itself, can only be assigned to any
and unknown
, but the reverse behavior is quite strange:
|
|
Due to the validity of case1 above, when the reducer’s second parameter payload
is defined by the user as any
, the generated dispatch function will have no parameters, which is clearly incorrect.
In fact, the compatibility of void
is better than never
, with never
only being compatible with itself, but any
behaves the same as void
. However, using []
later changes this:
|
|
I will use this point to submit a PR later to fix this issue.
Returning to the main topic, the inference logic is as follows:
- If the user hasn’t defined parameters, or only used state -> the dispatch parameter is empty.
- Otherwise, extract the first parameter
payload
and the second parametermeta
:- If
meta
is not defined, i.e.,[TMeta] extends [void]
:payload
is optional, i.e.,undefined extends TPayload
-> dispatch with an optionalpayload
- Otherwise -> dispatch with a mandatory
payload
- Otherwise, extract
TMeta
and determine:- If both
meta
andpayload
are optional, i.e.,[undefined, undefined] extends [TPayload, TMeta]
-> dispatch with both parameters optional - Otherwise,
payload
is mandatory, and determineTMeta
:TMeta
is optional, i.e.,undefined extends TMeta
-> dispatch with mandatorypayload
and optionalmeta
- Otherwise -> dispatch with both parameters mandatory
- If both
- If
It can be observed that we made an optimization above. For instance, even if the user defines (state, payload: number | undefined)
, the corresponding payload
in the generated dispatch function will be optional, which is logically reasonable (this deserves more discussion, and some people believe that such parameters should be mandatory, even if passing an undefined
). But the normal definition is still payload?: number
.
Reducer inference is relatively simple because state
is the first parameter, and the user-defined parameters are all behind it. But effect inference is more complex, mainly reflected in two aspects:
- An effect can be an object, that is, the
ModelEffects
mentioned above, or a functionModelEffectsCreator
. - In the effect, the
rootState
parameter is second,payload
is first, andmeta
is last.
Regarding the second point, one might ask why not unify it. Because the original design considered practical usage. In reducers, the current modelState is generally needed, so it is placed as the first parameter. In effects, most of the time, only payload
is needed, so rootState
is moved to the middle, and the least used meta
is placed last.
Let’s look at effect inference:
|
|
First, we use extends (...args: any[])
to determine whether the effect is an object or a function. If it’s a function, we need to extract the return value; otherwise, use it directly. The key point to look at in the second step: here, I used a clever approach, which is to first judge the rootState
parameter. If it is undefined
, it means the user hasn’t defined this parameter, so we only need to consider TRest[0]
, which is payload
. Next, check the type of rootState
. Why use RematchRootState<TModels> extends TRest[1]
instead of the reverse? Because rootState
here is the second parameter, there is a situation: the user can define the first parameter payload
as optional, and TS does not allow mandatory parameters to follow optional ones, so rootState
also needs to be defined as optional. In this case, due to parameter contravariance, we must use the above order. More information can be found in this discussion. For why TS has such a restriction, there’s also a good answer.
If rootState
is valid, then extract TRest[0]
and TRest[2]
, along with the return information TReturn
, and pass it to EffectRematchDispatcher
. The subsequent steps are the same as for reducers, the only difference being the additional TReturn
. Effects allow users to define their own return values, while reducers must return a ReduxAction
.
Using
infer TRest
to extract parameters has another advantage: if the parameter is not defined, when passed toEffectRematchDispatcher
, if the generic uses a default value, the default value will be used. This is different from directly defining the parameter asundefined
.
5.2 【Resolved】type guard
The original issue can be viewed here.
Let’s first look at a code snippet:
|
|
The above error is indeed a problem with TS, and it still exists now. One solution is:
|
|
But the example I simplified at the time was more complex, and using the above solution didn’t work. Although this issue was later fixed in TS v4.3.x, what did I do at the time?
Since I used NonNullable
for the passed parameters, if the if
statement couldn’t construct a NonNullable
type, then I would construct it manually:
|
|
I found that using both the Non-null assertion operator and the Nullish coalescing operator could achieve the desired effect. The former is TS syntax, seemingly constructing a NonNullable
type for you, while the latter is JS syntax. After this operation, the value of hook
becomes undefined | NonNullable<hook>
, and then after passing through if
, undefined
is filtered out.
Especially the last method seemed quite magical, originally possibly being undefined
(I refer to it as implicit), now this “implicit undefined
” was converted into an explicit undefined
, which could be filtered out. Of course, this is just my description; I didn’t fully understand the specific reasons, and since TS later fixed this issue, I didn’t delve further into it.
5.3 【Resolved】distributive conditional types
The original issue can be viewed here.
This issue is actually related to the dispatcher inference mentioned above, and it was probably raised before I rewrote this part. At that time, I did not know what a distributive conditional type was, and as mentioned before, the type inference of Dispatcher
was overly complex (the front part was optimized by me later, the previous judgment was more complex). Looking at the large segments of conditional branches, I had no idea where the error originated from.
I remember using very naive methods at the time, manually breaking down the conditions and judging step by step, finally discovering the quirk. But since I didn’t know the concept of Distributive conditional types and searching was quite difficult, I eventually discovered this concept and suddenly everything became clear. Below is a simple example:
|
|
The official explanation is as follows:
Conditional types in which the checked type is a naked type parameter are called distributive conditional types. Distributive conditional types are automatically distributed over union types during instantiation. For example, an instantiation of T extends U ? X : Y with the type argument A | B | C for T is resolved as (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y).
I also inquired about why it was designed this way, and RyanCavanaugh’s answer is as follows:
Distributivity is usually what you want in those scenarios, so it’s the default when the checked type is a naked type parameter. This was in favor of making special syntax for distributivity/non-distributivity since, with this behavior, you just generally don’t have to think about it.
The general idea is that this is indeed a great design, and this design aligns with our regular thinking (which is why I didn’t notice anything unusual before), and this kind of distributive generics design can be used to develop many utility types:
|
|
Well-known types such as Pick
, Exclude
, etc., are all based on this design.
Having discussed so many resolved issues, there are actually many unresolved ones, some limited by my current abilities, and some by TS limitations. I would be extremely grateful if everyone could help contribute to Rematch after reading this! Let’s take a look at those unresolved issues next.
5.4 【Unresolved】circularly reference
The original issue can be viewed here.
I previously mentioned Circularly Reference, mainly regarding the Models
type. But there’s another kind of circular reference situation:
|
|
As you can see, in the effectsB
style, TS reports a circular reference error, but this is actually OK at the code level, because the writing of effectsB
and effectsA
is almost identical. The only difference is that effectsA
always accesses foo
through dispatch
, while effectsB
first destructures, which may cause foo
in effectsB
to always be undefined
, as foo
might be added to dispatch
later. This problem can be avoided using the effectsA
method. Rematch once had such an issue.
Although the above problem was resolved (the solution can be viewed in the issue link), TS still considers there to be a circular reference:
- The type of
Root['foo']
istypeof foo
- To know the type of
foo
, the parameter type ofeffectsB
is needed- The parameter type of
effectsB
isExtract<Root>
- The destructured
foo
type isExtract<Root>['foo']
, i.e.,Root['foo']
- Back to the first
- The destructured
- The parameter type of
- To know the type of
This problem has not been resolved, and there are several similar issues, some quite strange. Interested readers can refer to some of the following comments:
- https://github.com/microsoft/TypeScript/issues/35546#issuecomment-611485331
- https://github.com/microsoft/TypeScript/issues/35546#issuecomment-645036905
- https://github.com/microsoft/TypeScript/issues/35546#issuecomment-668287404
5.5 【Unresolved】partial arg inference
The original issue can be viewed here.
This issue actually involves two problems, one of which is the same as mentioned in the section on the createModel
utility function, related to partial type parameter inference. The other problem was that I thought too simply, like in the following simplified example:
|
|
In foo
from the example above, I originally thought using the default parameter payload = 1
in setSomething
could infer payload
as number
. I later realized that since we have type constraints on reducers
in createModel
, whether using default parameter assignment or explicitly declaring the parameter type, we need to ensure that this type is compatible with the constrained type. But default assignment does not change the inferred type.
Actually, in Rematch’s code, the type of Reducer
is as follows:
|
|
As mentioned earlier, since partial type inference is not possible (i.e., TState
uses the user-defined generic, while payload
is inferred), we defined payload
as an optional any
type. This way, since any type is compatible with any
, users can narrow down its type when actually defining payload
.
The optional and mandatory effects of
payload
above are the same, becauseundefined | any
equalsany
.
5.6 【Unresolved】Type Design for Reducer and Effect with the Same Name
As mentioned earlier, in Rematch, reducer
can be named the same as effect
. Moreover, when called, the reducer
is executed first, followed by the effect
. I’m not sure why it was designed this way initially, and this behavior is hidden from users.
In addition, this posed a significant challenge to type design, or even it was impossible to achieve. Below is a code snippet from the effect middleware:
|
|
The above code first determines whether action.type
exists in effects
. If so, it calls the reducer
first (next(action)
represents calling the next middleware, with the reducer being executed last).
So, how should we consider this at the type level? Use a union to suggest two types? Or consider providing only the reducer type? I don’t think either solution is quite reasonable, although Sergio is currently using solution 2, which was mentioned earlier when discussing RematchDispatch. Let’s review it here:
|
|
Due to the setting of isEffect: boolean
, the same name reducer and effect lead to the appearance of the never
type, making the function uncallable. So we switched to the MergeExclusive
utility type for this task, which is relatively easy to understand. For example, for two types A
and B
, the end result is using union to replace intersection, but before the union, it does two things to A
and B
:
- For
A
, set all attributes different fromB
to?: never
(since optional is equivalent toundefined
, here it’s actuallyundefined | never
, i.e.,undefined
) - Then intersect with
B
Do the same for B
and then unite them.
Returning to the front, what are the advantages and disadvantages of this method? The advantage is that when reducer and effect have the same name but completely different payloads
, the use of a union type will cause payload
to be never
, thus uncallable, as seen in actionsAfter.decrement(1)
above. Why is this an advantage? Because it perfectly matches Rematch’s design, as the action processed first by the reducer will continue to be passed to the effect. If the payload
types of the two are completely different, it obviously could lead to errors.
What about the disadvantages? The disadvantage is also due to the union type. Look at actionsAfter.increment(1)
above. The payload
type of the reducer is a subset of the effect’s payload
type, which is expected. For example, it will prompt payload: number
, allowing the code to execute normally. But if reversed, TS can still ensure the successful execution of the code. However, since the reducer is executed first, the intention is to prompt the reducer type, but here it prompts the effect type, as seen in actionsAfter.incrementCopy('1')
above.
Honestly, reducers and effects with the same name are indeed strange, and we should avoid this situation.
5.7 【Unresolved】this
types
The original issue can be viewed here.
In the third article of this column, when discussing Rematch’s core plugins, I mentioned that the context this
in the effect function is bound to dispatch[modelName]
. This approach conveniently allows the use of this
in the effect to dispatch all actions of the current model. However, this also poses challenges to type compatibility at the TypeScript (TS) level. The current definition of the effect type is as follows:
|
|
If it were changed to:
|
|
In that case, almost all types would need to add the TModel
generic parameter, and it would also cause Model
to become like this:
|
|
This leads to Model
also circularly referencing itself, similar to the Circularly Reference section mentioned earlier with Models
. And as I mentioned, perhaps we can use this
to represent the type itself in the type definition. If feasible, I think this approach can also be applied to Model
. Currently, I do not have much time, but I will continue to pay attention to these two issues.
5.8 【Unresolved】select plugin types
The last unresolved issue is how to perfect the type definition for the select plugin. The code written by the original author of this plugin is quite complex, even more so than the reselect source code, which I find simpler. I cannot fully comprehend it, hence the type definition of this plugin is only slightly improved, and many parts are not thoroughly worked out. If anyone is interested, it would be great to delve into it and possibly fix it.
6 Summary
In fact, at the beginning of the reconstruction of the Rematch type system, my skill level was quite limited, so you will see that many of my designs were coincidental or accidental discoveries, just finding that they worked, which felt quite miraculous. But through writing this article, I traced back many of these “strange” designs and discovered more interesting aspects of TS. I hope everyone learns not only what things are but also understands why they are the way they are, and keeps the passion for learning!
This concludes this column. I hope that by reading all the articles, everyone can gain a deeper understanding of Rematch, thereby efficiently developing and mastering state management.