前言
这是一个我在基于TS
编写一种配置型组件模型时所想到的一种类型推导,个人觉得还是挺有意思的,在推导过程中对TS
类型编程的理解又加深了不少;
一个思考题
已知有一个递归数据结构如下:
1 2 3 4 5 6
| export interface Item { key: { [name: string]: string | number; }; children: Item[]; }
|
然后最外层的结构也是一个数组,即:Item[]
;
希望通过一个TS
泛型(即类型函数)将该递归数组结构转换为递归的对象结构,即树状结构;有以下满足上述数组结构的数据:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53
| const demo = [ { key: { name: '' }, children: [ { key: { other: '' }, children: [ { key: { age: 1 }, children: [] }, { key: { sex: '' }, children: [] } ] } ] }, { key: { size: {} }, children: [ { key: { width: 0 }, children: [] }, { key: { height: 0 }, children: [] } ] }, { key: { sss: 12 }, children: [] } ]
|
然后有一个函数形状为:
1
| function transfer<T extends any[]> (arr: T): Transfer<T>
|
通过该函数能够将变量demo
转为树状层级结构的数据:
1
| const target = transfer(demo)
|
因此期望得到的target TS推导结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| const target: { name: { other: { age: number; sex: string; }; }; size: { width: number; height: number; }; sss: number; }
|
请思考如何实现Transfer
泛型,使得推导结果满足上述要求。
注:这个类型推导很有挑战性,不久前我还一度认为TS
不可能推导出来,事实上是TS
确实有这个能力;如果能够写出Transfer
泛型,基本上对于TS
类型编程也就不会太吃力了;
背景
因为在尝试一个配置式复杂组件的数据层抽象,这个配置文件的结构是一个带层级信息的递归数组结构,跟上面的描述差不多,只是还多了一些很多其他的属性;期望从这个配置数据能够得到一个树状的对象结构,该对象就是组件具体承载的数据,以便输出调用,甚至响应化;
所以希望TS
能够从数组结构自动推导出该对象的数据结构,以更加方便地调用该数据,即数据结构透明化;由于输入的数组配置实际上是动态的,因此用TS
推导起来还是颇具难度的。
推导过程
由于TS
本身语法的限制,最后得到的推导路线确实是比较曲折的,其中有不少的细节还是值得注意的;
为何不能用带类型标注的类型结构
假如我们得到的层级数组结构一开始就被标记成自定义的类型,或者在参数传递过程中被推导成了自定义类型,那么后续的推导就变得不再具体起来,因为赋予了类型的结构在推导过程中会被TS
默认识别成自定义类型,而非根据实际值来推导得到的具体键值对结构;
如何处理推导中出现的never和unknown
由于推导数组元素的联合类型时,空数组会被自动推导成never[]
,而never
类型在跟其它类型进行组合时会得到意外不到的类型,因此需要特别处理never
类型的出现;
unknown
则会在使用一些自定义泛型时,TS
得不到一个透明的推导结构,因此会用unknown
来代替;其危害性在于会阻碍后续结构的推导,因此也需要特殊处理;
所以问题来了,那么怎样判断一个类型是never
或unknown
类型呢?当然本能地会想到使用extends
语法来判断,用这个来判断never
好像没啥问题,但是用这个来判断unknown
类型就有点问题了;
这样使用extends
来判断unknown
类型就会永远只得到unknown
;不过后面仔细想了想,这是由于unknown
是TS
中的顶级类型,因此非any
类型自然都会满足条件判断,因此换个位置就好了;不过,为了安全起见,想了一个泛型函数来进行同类型的判断:
1
| type IsSame<A, B> = (A | B) extends (A & B) ? true : false
|
这里为啥要把A & B
放在后面也是有点原因的,因为当两个原始类型不同时,进行交叉操作就会得到never
类型,而never
是一个底层类型,即never
是所有类型的子类型,因此放在extends
前面就会造成误判;
联合类型转交叉类型
联合类型转成相应的交叉类型,这也是一个技巧,这主要是利用了TS
目前唯一可用的逆变:即函数参数的类型兼容性(实际上是双向协变);
1 2 3 4 5
|
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
|
至于为啥要把联合类型转成交叉类型,是因为数组推导的是元素联合类型,一个成员类型只是对应目前对象中的一个属性,因此需要进一步合并每个属性得到对象;
infer和extends的注意点
infer
只能用在extends
语句的后面;
extends
本质上是判断继承关系,和常用的相等判断是有区别的;
为何会有过滤可选属性这一步骤
因为TS在对数组结构进行推导时,会自动合并同层级元素对象的属性,将某个元素不存在的属性,但是其他元素中存在的属性,变成其可选属性:
1 2 3 4 5 6 7 8 9 10 11 12
| const demo = [ { key: { a: '' } }, { key: { b: 1 } } ]
|
上述数组结构的自动推导结构为:
可以看到,原本在元素对象中不存在的属性自动就合并成可选属性了,这样的推导结果并不太准确,因为最好把其中的可选属性过滤掉;那么问题来了,怎么过滤掉对象类型中的可选属性?
1 2 3 4
| interface Demo { a: string; b?: number; }
|
由于一旦给属性加上了可选标识,其类型值会自动被加上undefined
类型,因此可以利用这个特点来简单地判断一个可选属性:
1 2 3 4 5 6 7 8 9 10 11
|
type KeepRequiredKey<T> = { [K in keyof T]: undefined extends T[K] ? never : K; }[keyof T]
type KeepRequired<T> = IsSame<KeepRequiredKey<T>, unknown> extends true ? T : Pick<T, KeepRequiredKey<T>>
|
推导代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
| export interface ABC { key: { [name: string]: string | number; }; children: ABC[]; }
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never
type KeepRequiredKey<T> = { [K in keyof T]: undefined extends T[K] ? never : K; }[keyof T]
type KeepRequired<T> = IsSame<KeepRequiredKey<T>, unknown> extends true ? T : Pick<T, KeepRequiredKey<T>>
type Compute<T> = T extends Function ? T : { [K in keyof T]: T[K] }
type IsSame<A, B> = (A | B) extends (A & B) ? true : false
type ABCObjectUnion<T extends any[]> = T extends Array<infer P> ? (P extends { key: infer K, children: infer C } ? (IsSame<C, never[]> extends true ? KeepRequired<K> : KeepRequired<{ [CK in keyof K]: C extends any[] ? KeepRequired<ABCObject<C>> : never }>) : never) : never
type ABCObject <T extends any[]> = Compute<UnionToIntersection<ABCObjectUnion<T>>>
function test2<T extends any[]> (a: T): ABCObject<T> { return a as any as ABCObject<T> }
const t1 = [ { key: { name: '' }, children: [ { key: { other: '' }, children: [ { key: { age: 1 }, children: [] }, { key: { sex: '' }, children: [] } ] } ] }, { key: { size: {} }, children: [ { key: { width: 0 }, children: [] }, { key: { height: 0 }, children: [] } ] }, { key: { sss: 12 }, children: [] } ]
const c = test2(t1)
c.name.other.age c.size.width
|
TS
推导后的结构为:
显然是想要的目标对象结构,说明这种推导时可行的;因此在使用该推导结构时vscode
都会有相应的提示,十分便利;
相关文档