服务器返回的对象可能包含容器属性。
想象一个像这样的物体
{
attr1: string,
attr2: number,
attr3: {
uselessArg1: ...,
uselessArg2: ...,
uselessArg3: ...,
content: [subVal1, subVal2, ...]
}
}
我有一个函数,可以抓取那个对象并像这样返回:
{
attr1: string,
attr2: number,
attr3: [subVal1, subVal2, ...]
}
现在,真正的问题是,任何对象都可能有一个属性,该属性的对象类型带有无用的键和content
子属性,包括redundant
数组中的对象
因此,我们可以想象,任何给定的对象都继承自这个接口:
interface MaybeHasContent {
[key:string]?: {
uselessArg1: ...,
uselessArg2: ...,
uselessArg3: ...,
content:this[]
}
}
我的实用程序函数首先验证对象中的任何键是否有content
子键,如果找到,则将content
子键的值提升到其父键,这也会清除所有无用的属性。然后,它继续对当前工作对象的任何子对象以及content
数组内的任何对象调用自己。它基本上贯穿对象的所有分支,包括具有对象阵列的分支,并将一个非常特定的密钥提升一级
我该如何为这个怪物编写界面?我想让TS明白,如果我有一个可以写myObj.val1.redundant[0].name
的对象,那么在通过我的实用程序函数传递它之后,这将是一个合法的调用:myObj.val1[0].name
这里有一种可能的方法:
declare function clean<T>(t: T): Clean<T>;
type Clean<T> = T extends object ? (
T extends { content: infer U } ?
Clean<U> : { [K in keyof T]: Clean<T[K]> }
) : T;
函数采用泛型类型T
的值t
,并返回类型Clean<T>
的值。
而CCD_ 11是表示CCD_ 12的作用的递归条件类型。如果T
是非基元object
类型,那么我们检查是否具有content
属性。如果是,我们提取该属性(使用条件类型infer
ence(,并将其作为新的类型参数传递给Clean
。如果不是,那么我们将对象类型T
映射到每个属性值都应用了Clean
的版本。(注意,这也涉及数组类型,因为数组和元组上的映射类型变成了数组和元组。(最后,如果T
是基元类型,我们只返回T
。
让我们测试一下:
const cleaned = clean({
attr1: "abc",
attr2: 123,
attr3: {
uselessArg1: 1,
uselessArg2: 2,
uselessArg3: 3,
content: [
{ a: 1, b: 2, c: { content: "a", x: 1 } },
{ a: 3, b: 4, c: { content: "b", x: 2 } }
]
}
});
/* const cleaned: {
attr1: string;
attr2: number;
attr3: {
a: number;
b: number;
c: string;
}[];
} */
看起来不错。CCD_ 22的类型与具有所有CCD_;"吊起来";。因此attr3
不具有uselessArg
属性。它只是一个数组。该数组的元素有一个c
属性,它只是string
,因为输入数组的c
属性本身就有一个值为string
的content
属性。
此类递归类型实用程序总是存在边缘情况。例如,如果你试图清理一个病态的递归数据结构,那将是一个坏主意:
interface ContentOuroboros {
content: ContentOuroboros
}
type OhNoes = Clean<ContentOuroboros> // error!
// Type instantiation is excessively deep and possibly infinite.
我想你不太可能这么做,但我很难提前想到所有可能的陷阱。因此,一定要对照您的用例进行测试,并在必要时进行调整。
游乐场链接到代码