【Rematch 源码系列】番外一、详解 Rematch Dispatcher 类型体操

Rematch 源码解读系列,番外的第 1️⃣ 篇,和大家聊下 Rematch Dispatcher 的 TypeScript 类型实现思路。
前段时间修复了 Rematch 的一个类型 bug。通过这个 bug,又发现了 Rematch Dispatcher 尚存的几个类型 bug,且代码组织上也有冗余。借这个机会,我重构了这部分,并且发现了一些有意思的部分。
1 问题复现
下面是一个简单的复现例子,你也可以点击 Playground。
| |
在 ExtractDispatcherFromReducer 里,我是打算使用 TRest[0] 获取到 payload 参数,TRest[1] 获取到 meta 参数。且以为当没有定义 payload 或者 meta 参数时,选中的 ReducerDispatcher 相应泛型参数会被设置为默认值 void。然而事实与我想的不一样,最终传入的参数其实会是 undefined。且因为 [undefined] extends [void] === true,效果上来看和 [void] extends [void] === true 并无不同,所以这个 bug 也一直没被发现。
不过,一旦 payload 类型为 any,这个 bug 便无处藏身了,由于 [any] extends [void] === true,最终进入到 () => void 这个分支,此时 payload 参数推导为空,和定义不符。
注意:
any extends void === true | false。
那这个问题如何解决?其实只需找到一个类型 T 使得 [any] extends [T] === false。没错,我们可以使用 never 来替换 void。
注意:
any extends never === true | false。
到这里为止,上面这个特定的 bug 就解决了。但还遗留两个问题,首先是前面提到的,当没有定义 meta 参数时,TRest[1] 取到并传入 ReducerDispatcher 的永远是 undefined,并不会触发默认赋值 never,这样一来推导的参数始终包含可选的 meta。我们可以对 ReducerDispatcher 优化如下:
| |
由于新增了一个分支判断,此时 TRest[1] 不传入 ReducerDispatcher,TMeta 可以被默认赋值为 never。
第二个问题则是我们无法区分用户自定义的 undefined 类型,一旦用户定义 payload 或 meta 的类型为 undefined | T(虽然这样做毫无意义,因为可直接使用可选符号 ?),那么上面的类型体操也将失效,表现为即使用户定义了 undefined | T 类型,推导出来的参数会变成可选参数。
最开始的类型设计中,为了简化推导,不管用户定义的是可选参数还是明确的 undefined 类型,Rematch Dispatcher 推导后的均为可选参数。这样做并不会导致 runtime error,且也没有用户反馈过相关问题。不过这样做和 TS 的行为并不一致,出于精益求精的考量,我打算这次想办法对这俩进行区分。
2 区分 undefined 类型和可选参数
一旦我们可以用体操将用户自定义的 undefined 类型和可选参数形式区分开来,那么我们便可以做到完美的推导。这里的难点在于如何在提取参数的同时保留参数的可选特性。一旦我们使用索引来访问具体参数,它的可选特性便会消失:
| |
2.1 保留可选参数的数组结构
这也给了我启示,如果想要保留参数的可选性质,则不能单独进行提取,而是要保留外面的数组结构:
| |
保留了参数的可选性质,接下来我们要进行判断和提取。
2.2 判断和提取可选参数
下面 2 条规则可以帮助我们梳理思路:
- 参数多的数组无法赋值给参数少的,例如
[payload: unknown, meta?: unknown] extends [payload: unknown] ? 1 : 0的结果将会是0。 - 含有必选参数的数组可以赋值给含有可选参数的数组,例如
[payload: unknown] extends [payload?: unknown] ? 1 : 0的结果是1,但反之不成立。
注意:如果使用 Parameters 或 infer 来获取参数类型,上面第 2 点是成立的,但如果直接写成数组形式,则不成立,见下方例子
| |
不知道上面是否是一个 BUG,我也还没找到答案。不过 Rematch 里参数数组均使用 infer 提取,因此暂无影响。
使用上面的思路,我们可以先通过条件类型先判断参数少的情况(第一点),进而继续判断出必选参数的情况(第二点)。下面分别看看 Rematch 中针对 reducer 和 effect 的条件判断。
2.3 从 reducer 中提取参数
由于我们并不需要 reducer 的第一个参数 state,因此先将它忽略,然后把剩余参数全部传入我们新增的一个类型 ExtractParametersFromReducer 中处理即可。这样一来前面提到的 ExtractDispatcherFromReducer 类型也可以得到简化,因为我们把参数交给专门的 ExtractParametersFromReducer 处理,它将会变得很简洁。
此外,Rematch 中的 RematchDispatcher 类型也得到了极大简化,因为在之前的逻辑中,RematchDispatcher 和 ExtractDispatcherFromReducer 都对参数进行了判断,代码是冗余的,而如今类型 ExtractParametersFromReducer 的输入输出均为参数数组,输入无需提前处理,输出亦可以直接使用。详情可以点击这一条 PR 查看。
下面是相关代码:
| |
细心的同学可以看到我在 ExtractParametersFromReducer 中除了 infer TPayload 还有一个 infer TPayloadMayUndefined,这是因为如果用户定义的参数为 p: number | undefined 时,TPayload 类型只有 number 而 undefined 被忽略掉了。因此这里我们使用 TPayloadMayUndefined 来正确推导这种情况。(下面的 TMetaMayUndefined 也是一样)
2.4 从 effect 中提取参数
Rematch 的 effects 中我们需要忽略的是第二个参数 rootState。由于不太方便直接去掉第二个参数类型,因此我们把所有参数全部交给对应的 ExtractParametersFromEffect 处理。代码如下:
| |
effect 的情况和 reducer 大致相同,在只含 payload 的情况下,第二个参数 s 我们在条件中定义为可选即可,这样不管它是否被定义均能命中该分支。然后是三个参数及以上的情况,先判断必选,最后判断可选,和 reducer 一致。
effect在最初设计时,考虑到payload是个需要频繁使用的参数,因此把他放到了第一位,而rootState放到第二位,后面增加的meta则自然放入了第三位,且meta使用频率不高。对于reducer,当前model的state一般而言访问频率更高,因此将它放到第一位,而payload和meta则分列二三位。
3 总结
在处理这种体操问题时,重要的是先想到是否有约束可以逐渐缩小可能的范围。比如通过「参数多的数组无法赋值给参数少的」这条规则,我们便可以使用「参数少的数组」这条约束来先处理参数少的情况,再逐步处理参数多的情况。而在各自内部,我们继续通过「含有可选参数的数组无法赋值给含有必选参数的」这条规则,使用「必选参数的数组」这条约束来先处理必选参数的情况。
同时,对于多个参数,由于 TS 中可选参数只能位于必选参数之后,我们优先处理末尾参数可选的情况,最后再处理所有参数都可选的情况。
只要找到了这样的规律,你就是这类体操的冠军 🏆!




