TypeScript 类型体操:合并映射类型的处理结果为联合类型
索引类型是 TypeScript 中的常见类型,它是聚合多个元素的类型,对象、类、元组等都是索引类型。
比如这样:
type Person = {
name: string;
age: number;
hobbies: string[]
}
对索引类型做变换会用到映射类型的语法,它可以对索引类型的索引和值做一些变换,然后产生新的索引类型。
比如给每个索引加上 readonly 的修饰:
type ToReadonly<Obj> = {
readonly [Key in keyof Obj]: Obj[Key]
}
返回的就是修改后的索引类型:
TypeScript 也内置了很多基于映射类型实现的工具类型,比如 Partial、Required 等。
总之,会了映射类型就能够对索引类型做各种变换了。
但是,这些都是对索引类型整体做的变换,变换的结果依然是一个索引类型。
有的时候是想把它们分开的。比如这种需求:
希望能把每个索引给分开。
这种怎么处理呢?
我是这样写的:
type SplitObj<Obj> = {
[Key in keyof Obj]: {
[Key2 in Key]: Obj[Key2]
}
}[keyof Obj];
先看结果:
确实把每个索引给分开了。
再来讲为什么:
keyof Obj 我们知道是 key 构成的联合类型 'name' | 'age' | 'height'。
外层映射类型 [Key in keyof Obj] 就是对每个 Key 做处理,它值也是一个映射类型,而 Key2 来自于刚才的 Key,那么这样映射完之后的类型就是这样的:
这时你取 name 的值就是这样的:
而传入联合类型的时候,会分别传入每个类型做处理,也就是这样的:
所以直接在这里取 keyof Obj 的所有索引值:
总结一下:当我们需要把索引分开的时候,可以加一层映射类型,在值的位置对每个索引做处理,然后再传入 keyof Xxx 来取处理过后的值的联合类型。
这种套路还是很有用的,比如下面这个更复杂一点的案例:
给你一个索引类型,让你拿到所有索引的路径。
怎么做呢?
这里明显要对每个索引都做路径的处理,然后把所有的路径合并。
根据刚才学过的写法,可以这样写:
type DFS<Obj> = {
[Key in keyof Obj]: Key
}[keyof Obj];
这样就能把每个索引分开处理:
然后具体的处理是需要递归的,如果值是索引类型就继续递归,否则就结束。递归过程中记录下路径。
type DFS<Obj> = {
[Key in keyof Obj]:
Key extends string
? Obj[Key] extends Record<string, any>
? Key | `${Key}.${DFS<Obj[Key]>}`
: Key
: never
}[keyof Obj];
这里因为 Key 默认推导出来的不是 string,所以加一层 Key extends string 的判断之后,再用 Key的时候就是 string 了。
判断 Obj[Key] 是不是索引类型,也就是是不是 Record<string, any>,如果是就递归调用 DFS,并且记录当前路径到结果里,如果不是就返回当前 Key。
这样的结果就是对每个索引做了递归的处理,并且把所有索引的处理结果合并到了一起:
回顾下这个案例,它也是要把每个索引的处理结果分开,通过联合类型合并在一起。具体每个索引是做递归的处理,记录路径。
总结
索引类型是 TypeScript 中的常见类型,可以通过映射类型的语法来对它做一些修改,生成新的索引类型。
但如果你想对每个索引分别做处理,并且把结果合并为一个联合类型的时候,可以加一层映射类型来分别处理每个索引,再取 keyof Xxx,也就是每个索引类型的处理结果构成的联合类型。
这种套路在需要把索引分开处理,再把结果合并的场景下是很有用的。
转载自:https://juejin.cn/post/7164407257151569956