> : {
[_ in CombineStringKey]:T[K]
}
}[StringKeyof]
```
我们将 `ChainKeys` 函数拆开来看,首先是它的参数,接受两个类型参数,第一个是类型T,第二个则是用于拼接的**前缀字符串**,其值可选。我们使用 `in` 语法遍历 `T` 的所有 key 值,也就是 `[K in StringKeyof]`,这里我们使用了前面封装好的方法 `StringKeyof` 来代替 `keyof` 关键字。对对应的 key 的值,也就是 `T[K]`,我们使用 `extends` 进行前置判断,如果 `T[K]` 是一个**复杂类型**,我们则将当前的字符串前缀 `P` 和当前的 `K` 进行拼接,递归调用 `ChainKeys` 函数进行处理。如果 `T[K]` 为**基本类型**,我们直接使用 `Record` 方法组装拼接好的key(`CombineStringKey`)和value(`T[K]`),为了方便在编辑器里面查看结果,这里将`Record, T[K]>` 替换为 `{ [_ in CombineStringKey]:T[K] }`。最后将所有结果打平成联合类型,也就是最后的一个取值操作:`{...}[StringKeyof]`,这和 `[1,2,3][number] // 3 | 1 | 2` 的处理方式一致。
接下来我们在编辑器中查看一下调用结果:

如红框中所示,发现链式key已经和其类型一一对应起来了。
但是这还有一些问题:
一、`"b"` 以及 `"b.e"` 没有出现在枚举当中。
二、返回结果是一个**联合类型**,如何把这个结果运用到函数当中呢?
第一个问题其实很好解决,当我们在递归处理的同时,将当前的链式key与对应值也进行处理就可以,即:
```ts
type ChainKeys = {
[K in StringKeyof]: T[K] extends Record
? Record, T[K]> & ChainKeys>
: {
[_ in CombineStringKey]:T[K]
}
}[StringKeyof]
```
同样为了方便编辑器中查看结果我们把 `Record, T[K]>` 替换成 `{ [_ in CombineStringKey]:T[K] }`:

可以看到 `b` 以及 `"b.e"` 这样的key都被取出来并放到了结果当中。其实这里还是有一些问题,如果你实际去代码编辑器里面查看结果的话,会发现有很多重复的 `b` ,这是因为后续使用交叉类型进行拼接的 `ChainKeys` 的返回值是一个联合类型,如此一来,`b` 会被分配到联合类型的**每一项**上,在后面我们会解决这个问题。
第二个问题是结果为联合类型,我们如何才能将其运用至函数当中呢?
大家都知道,对联合类型使用 `keyof` 操作,返回的类型为 `never`,即无法取出联合类型的所有key值:
```ts
type T = { a: number } | { b: string }
type K = keyof T // never
// 如何使K的类型为 a | b 呢?
```
这里我们引入另一个工具函数:`UnionToIntersection`,顾名思义,该函数的作用就是将联合类型转换为交叉类型,至于**为什么**它能将联合类型转换为交叉类型,我们后面会讲:
```ts
type UnionToIntersection = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;
```
我们将其运用到ChainKeys的推导当中去:
```ts
type ChainKeys = UnionToIntersection<{
[K in StringKeyof]: T[K] extends Record
? Record, T[K]> & ChainKeys>
: {
[_ in CombineStringKey]:T[K]
}
}[StringKeyof]>
```
再经过一些润色得到一个比较精简的函数:
```ts
type ChainKeys = UnionToIntersection<{
[K in StringKeyof]: Record, T[K]> & (T[K] extends Record ? ChainKeys> : {})
}[StringKeyof]>
```
尝试调用该方法最终得到的结果为:

最终,我们以此为基础,编写 `getValueByPath` 函数的定义:
```ts
declare function getValueByPath, K extends keyof ChainKeys>(object: T, prop: K): ChainKeys[K]
let a = getValueByPath({ a: { b: { c: 1 } } }, 'a') // { b: { c: number } }
let b = getValueByPath({ a: { b: { c: 1 } } }, 'a.b') // { c: number }
let c = getValueByPath({ a: { b: { c: 1 } } }, 'a.b.c') // number
```
到此,我们算是完成了 `getValueByPath` 函数的类型定义以及推导。
## 深层次Actions的dispatch函数TS实现
其实在推导 `getValueByPath` 的过程中,我们已经基本上完成了所有实现该 `dispatch` 所需要的条件,与 `getValueByPath` 不同的是,我们只需要遍历出函数所对应的链式key即可,改写一下 `ChainKeys` 方法:
```ts
interface DeeperActions {
[k: string]: ((...args: any[]) => any) | DeeperActions
}
type ActionChainKeys = UnionToIntersection<{
[K in StringKeyof]: T[K] extends DeeperActions
? ActionChainKeys>
: Record, T[K]>
}[StringKeyof]>
```
我们除去了对对象的混入,同时将判断类型改为了 `DeeperActions` ,以此来判断该 `action` 对象是否有更深层次的actions。
调用结果:

再看我们最开始提出的问题:
> 编写一个 dispatch 方法,使得:
>
> `let result = dispatch('deeperActions.anotherAction') // 返回类型为: Promise`
>
> `let final = dispatch('deeperActions.otherActions.finalAction', 'hello world') // 返回类型为:Promise`
我们开始定义 `dispatch` 方法:
```ts
declare function dispatch(key: K, ...args: Parameters): ReturnType
```
这里我们分别使用了 `Parameters` 和 `ReturnType` 函数来获取对应 `action` 的参数以及返回值,用来填充至 `dispatch` 方法中,实际编写调用代码:


考虑到我们的 `actions` 无论如何都会返回一个 `Promise`,上述定义其实还是稍微有一些问题,但是改动起来也很方便,只需要在 `ReturnType` 外层使用 `Promise` 包装一下就可以了:
```ts
type Promisify = T extends Promise ? T : Promise
declare function dispatch(key: K, ...args: Parameters): Promisify>
let result = dispatch('deeperActions.anotherAction') // Promise
let final = dispatch('deeperActions.otherActions.finalAction', 'hello world') // Promise
```
至此,我们 `dispatch` 的TS实现也已经完成。
最终结果:

* 若图片加载失败请访问: http://cdn.qiniu.archerk.com.cn/QQ20210128-162139-HD.gif
## 结尾
其实 `getValueByPath` 这一部分的内容是有一些缺陷的,因为TS本身**限制了递归的次数**,当数据层级达到一定程度时,上文的推导会失效。在Mpx的建设中我们还对此做了一些优化,尽可能的**减少**了递归次数。
这里也给出**另一个方案**做链式key推导,这种方式**性能**上会好很多,因为不用解析出所有的链式key,而是根据链式key**反向解析**前面的对象,因此该方案可以解析出正确的类型,但是会丧失一部分代码提示:
```ts
type CutChainKeys = T extends `${infer A}.${infer B}` ? [A, B] : [T, never]
type GetValueByPath, K extends string> = CutChainKeys extends [string, never]
? T[CutChainKeys[0]]
: GetValueByPath[0]], CutChainKeys[1]>
declare function test, K extends string>(o: T, k: K): GetValueByPath
let s = test({
a: {
b: 1,
c: {
d: {
e: {
f: 'f'
}
}
}
}
}, 'a.c.d.e.f') // string
```
通过这一系列的处理,我们基本完成了 `mpx-store` 的类型推导,包括对应的**辅助函数**等。这也使得我们能够在项目中更好的使用TS进行开发。在最新的滴滴出行小程序更新中,我们已经开始使用TS全量进行开发,目前看来开发体验还不错,后期也会逐步将旧的代码重构为TS。我们后续也会持续维护和迭代框架,也欢迎有兴趣的朋友来一起[共建](https://github.com/didi/mpx)。
有写的不好的地方请多包含,也欢迎大家批评指正。
## 题外话:UnionToIntersection
上面提到的,我们使用工具函数 `UnionToIntersection` 将联合类型转换为交叉类型。而 `UnionToIntersection` 又是如何将**联合类型转换为交叉类型**的呢?
在 [Conditional Types](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html) 中有提到一些点:
### Distributive conditional types
> 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)`.
即在**执行** `extends` **语句时**,如果 `extends` 左侧为**联合类型**,则会被分配成**对应数量个**子条件判断语句,最终**联合**每个子语句的结果。
```ts
type A = 1 | '2' | 3
type IsNumber = T extends number ? T : never
type B = IsNumber // 1 | 3
```
### Type inference in conditional types
> Within the `extends` clause of a conditional type, it is now possible to have `infer` declarations that introduce a type variable to be inferred. Such inferred type variables may be referenced in the true branch of the conditional type. It is possible to have multiple `infer` locations for the same type variable.
在条件判断语句中,可以使用 `infer` 关键字做类型推断,同一个候选值能有**多个**推断位置。
```ts
type ReturnType