假设我有以下界面:
interface Foo {
foo: string
bar?: number
nested: {
foo: string,
deeplyNested: {
bar?: number
}
}
union: string | {
foo: string,
bar?: number
}
}
Run Code Online (Sandbox Code Playgroud)
现在我需要一种类型来将此接口转换为扁平版本,键中包含完整路径名,如下所示:
interface FooFlattened {
foo: string
bar?: number
"nested.foo": string
"nested.deeplyNested.bar"?: number
// if union is a string:
union: string
// if union is an object:
"union.foo": string
"union.bar"?: number
}
Run Code Online (Sandbox Code Playgroud)
该类型应该适用于任何级别的嵌套和任何数量的联合成员。
我已经在这里找到了一个相关的问题,但这只是一个非常基本的界面,没有联合或可选参数。
有很多方法可以解决这个问题,根据我的经验,它们都很棘手、繁琐、脆弱,并且有很多疯狂的边缘情况。请参阅如何展平具有嵌套子属性的对象类型?对于类似的问题和类似的充满警告的答案。
最后,针对这种情况我决定采取以下方法。Entry我们可以采用一个对象类型并将其表示为元素的联合,其中 anEntry具有key、value和optional属性:
type Entry = { key: string, value: any, optional: boolean };
Run Code Online (Sandbox Code Playgroud)
所以对于像你这样的类型,{a: string, b?: number}你会得到{key: "a", value: string, optional: false} | {key: "b", value: number | undefined, optional: true}.
第一步,我们将像Foo和Explode这样的输入接口放入一个大的元素联合中Entry,其中深度嵌套的属性将转换为带有点路径的单个键。那么Explode<{a: {b: string}}>就会变成{key: "a.b", value: string, optional: false}。这完成了算法的繁重工作,我们必须决定诸如可选属性如何传播、联合如何传播等问题。
的可能定义Explode如下:
type Explode<T> =
T extends object ? { [K in keyof T]-?:
K extends string ? Explode<T[K]> extends infer E ? E extends Entry ?
{
key: `${K}${E['key'] extends "" ? "" : "."}${E['key']}`,
value: E['value'],
optional: E['key'] extends "" ? {} extends Pick<T, K> ? true : false : E['optional']
}
: never : never : never
}[keyof T] : { key: "", value: T, optional: false }
Run Code Online (Sandbox Code Playgroud)
如果我们Explode是非对象类型,我们使用空白键;否则,我们递归地获取Explode对象的所有属性,然后使用模板文字类型连接键,并决定具有可选属性的内容(我认为我这样做是为了{a: {b?: string}}使可选属性成为b可选属性,但又{a?: {b: string}}使属性成为b必需属性)。一团糟。
哦,我不知道如何处理数组;我决定只使用"0"作为表示数组索引的键,因此我将其重命名Explode<T>为_Explode<T>,然后Explode<T>根据它进行定义:
type Explode<T> = _Explode<T extends readonly any[] ? { "0": T[number] } : T>;
Run Code Online (Sandbox Code Playgroud)
最后,一旦我们有了元素的完整分解联合Entry,我们就可以将Collapse它们组合成一个对象类型:
type Collapse<T extends Entry> = (
{ [E in Extract<T, { optional: false }> as E['key']]: E['value'] }
& Partial<{ [E in Extract<T, { optional: true }> as E['key']]: E['value'] }>
) extends infer O ? { [K in keyof O]: O[K] } : never
Run Code Online (Sandbox Code Playgroud)
我使用键重新映射 viaas来迭代对象E联合的T每个元素Entry;每个键都是E['key'],每个值都是E['value']。通过将联合分为具有属性true和false值的联合optional,我们可以分别生成具有可选属性和必需属性的输出类型。
最后,Flatten<T>这就是当你Explode进入Entry对象然后进入Collapse这些对象时你要做的事情:
type Flatten<T> = Collapse<Explode<T>>
Run Code Online (Sandbox Code Playgroud)
以下是它在您的示例中的工作原理:
type FooFlattened = Flatten<Foo>
/* type FooFlattened = {
foo: string;
"nested.foo": string;
union: string;
"union.foo": string;
bar?: number | undefined;
"nested.deeplyNested.bar"?: number | undefined;
"union.bar"?: number | undefined;
}*/
Run Code Online (Sandbox Code Playgroud)
它与您手动编写的预期输出完全相同(属性顺序除外,这不会影响类型相等性)。
对于更复杂的事情,比如
interface Foo {
tmdb: number | {
title: {
original: string
german?: string
}
budget?: number
revenue?: number
tagline?: string
overview?: string
productionCompanies?: {
id?: number
logoPath?: string
name?: string
originCountry?: string
}[]
releaseDate?: string
genres?: string[]
runtime?: number
poster?: string | {
data: { sample: any }
contentType: string
}
}
rating: { ch: number; rt: number } | { total: number }
dateSeen?: Date
fsk?: number
mm?: boolean
}
Run Code Online (Sandbox Code Playgroud)
我们得到
type FlattenedFoo = Flatten<Foo>
/* type FlattenedFoo = {
tmdb: number;
"tmdb.title.original": string;
"tmdb.genres.0": string;
"tmdb.poster.data.sample": any;
"tmdb.poster.contentType": string;
"rating.ch": number;
"rating.rt": number;
"rating.total": number;
"tmdb.title.german"?: string | undefined;
"tmdb.budget"?: number | undefined;
"tmdb.revenue"?: number | undefined;
"tmdb.tagline"?: string | undefined;
"tmdb.overview"?: string | undefined;
"tmdb.productionCompanies"?: undefined;
"tmdb.productionCompanies.0.id"?: number | undefined;
"tmdb.productionCompanies.0.logoPath"?: string | undefined;
"tmdb.productionCompanies.0.name"?: string | undefined;
"tmdb.productionCompanies.0.originCountry"?: string | undefined;
"tmdb.releaseDate"?: string | undefined;
"tmdb.genres"?: undefined;
"tmdb.runtime"?: number | undefined;
"tmdb.poster"?: string | undefined;
dateSeen?: undefined;
fsk?: number | undefined;
mm?: boolean | undefined;
} */
Run Code Online (Sandbox Code Playgroud)
我认为这是合理的。
当然,在某些极端情况下,这可能会导致您不想要的结果。它们可以通过改变Explode工作方式来解决,或者您所做的任何事情都无法满足您所有预期的用例,您将不得不决定要么放弃一些用例,要么放弃Flatten.
| 归档时间: |
|
| 查看次数: |
2092 次 |
| 最近记录: |