如何使用字符串联合填充对象类型的可选嵌套关系?
How can I populate an object type's optional nested relations using string unions?
我正在尝试创建一个采用 2 个泛型的 Populate
类型:具有可选关系的对象类型(引用其他对象类型的键),以及可以深度填充的路径字符串联合(或者更确切地说,设置为非可选)的关系。例如:有 3 个实体都可以选择相互引用:
type Tag = { name: string, countries?: Country[], companies?: Company[] }
type Company = { name: string, country?: Country, tags?: Tag[] };
type Country = { name: string, companies?: Company[], tags?: Tag[] };
type Populate = { // ... need help here ... // }
type CompanyWithCountry = Populate<Company, 'country'>
type CompanyWithCountryAndTags = Populate<Company, 'country' | 'tags' | 'country.tags'>
type SuperPopulatedCompany = Populate<Company, 'country' | 'country.tags', 'country.tags.companies' | 'tags' | 'tags.companies' | 'tags.countries'>
/** result for SuperPopulatedCompany = {
name: string;
country: null | Populate<Country, 'tags' | 'tags.companies'>, //note for non-array items `null would be possible`
tags: Populate<Tag, 'companies' | 'countries'>[], //for array relations at least an empty array is always returned
}
*/
这样做的目的是让我能够在我的一些查询中使用 TypeORM 的 relations
键键入结果对象,这可以填充相互引用的关系对象。不幸的是,TypeORM 的 return 类型始终是基本实体类型,并且无论您将哪些关系传递到查询中,任何关系始终保持可选。我想转换 return 类型以使关系在被查询时不是可选的。例如:
const company = companyRepository.find({
id: 1,
relations: ['country', 'country.companies', 'country.tags', 'tags', 'tags.companies'],
}) as Populate<Company, 'country' | 'country.companies' | 'country.tags' | 'tags' | 'tags.companies' >
/*
typeof company = {
name: string;
country: null | {
name: string;
companies: Company[], //non-optional
tags: Tags[], //non-optional
}
tags: {
name: string;
companies: Company[], // non-optional
countries?: Country[], //optional (because not in relations query)
}[]
}
Allowing me to access:
company.country,
company.country?.companies,
company.country?.tags,
company.tags,
company.tags[n]?.companies
*/
让我们首先定义两个将要使用的实用程序类型:
type SplitPath<S, R extends unknown[] = []> = S extends `${infer P}.${infer Rest}` ? SplitPath<Rest, [...R, P]> : [...R, S];
type JoinPath<P, S extends string = ""> = P extends [infer First, ...infer Rest] ? JoinPath<Rest, `${S}${S extends "" ? "" : "."}${First & string}`> : S;
这些类型简单地拆分和连接路径(彼此相反)。
SplitPath<"country.tags.companies">
会给我一个元组 ["country", "tags", "companies"]
JoinPath<["country", "tags", "companies"]>
会给我一个字符串 "country.tags.companies"
然后一个类型递归扩展给定类型以进行调试:
// Type to expand results so we can see if it works (mostly for debugging)
// WARNING: BREAKS ON TUPLES
type Expand<T> = T extends ReadonlyArray<unknown> ? Expand<T[number]>[] : T extends object ? { [K in keyof T]: Expand<T[K]> } : T;
没有它,当我们尝试查看 Populate
的结果时,它不会显示结果,而是实际显示 Populate<Company, [...]>
,这没有帮助。使用这种类型,它将 Populate<...>
扩展为我们可以看到的完整形式。
现在我们定义一个简单的find
函数来测试Populate
:
declare function find<Relations extends ReadonlyArray<string> = never>(criteria: {
id?: number;
relations?: Relations;
// ...
}): Expand<Populate<Company, Relations>>;
因为 relations
是可选的,所以我为 Relations
通用参数添加了默认值 never
。然后当你不提供关系时 Populate
没有效果。
填充 Company
后,我们将其展开为完整形式。
这里是 Populate
,乍一看似乎令人生畏:
type Populate<T, Keys extends ReadonlyArray<string>> = Omit<T, SplitPath<Keys[number]>[0]> & {
[K in SplitPath<Keys[number]>[0]]-?:
NonNullable<T[K & keyof T]> extends ReadonlyArray<unknown>
? Populate<NonNullable<T[K & keyof T]>[number], OmitFirstLevel<Keys, K>>[]
: NonNullable<Populate<NonNullable<T[K & keyof T]>, OmitFirstLevel<Keys, K>>>
};
首先,我们忽略 T
中所有受影响的属性,因为我们稍后会添加受影响的属性:
Omit<T, SplitPath<Keys[number]>[0]>
让我们使用一个简单的示例 Populate<Company, ["country", "country.tags"]>
来了解 Populate
的工作原理并简化此 Omit
用法:
Omit<Company, SplitPath<["country", "country.tags"][number]>[0]>
Omit<Company, SplitPath<"country" | "country.tags">[0]>
Omit<Company, (["country"] | ["country", "tags"])[0]>
Omit<Company, "country">
在我们省略所有受影响的 first-level 键之后,我们将其与映射类型相交以将它们添加回来:
{
[K in SplitPath<Keys[number]>[0]]-?:
NonNullable<T[K & keyof T]> extends ReadonlyArray<unknown>
? Populate<NonNullable<T[K & keyof T]>[number], OmitFirstLevel<Keys, K>>[]
: NonNullable<Populate<NonNullable<T[K & keyof T]>, OmitFirstLevel<Keys, K>>>
}
再一次,为了理解我们在这里做什么,让我们简化一下 step-by-step。所以首先像上面一样,我们得到受影响的 first-level 键并映射它们:
{
[K in "country"]-?:
NonNullable<T[K & keyof T]> extends ReadonlyArray<unknown>
? Populate<NonNullable<T[K & keyof T]>[number], OmitFirstLevel<Keys, K>>[]
: NonNullable<Populate<NonNullable<T[K & keyof T]>, OmitFirstLevel<Keys, K>>>
}
-?
要求 属性。没有它,它仍然是可选的。
所以现在,映射类型简化为:
{
country: NonNullable<T["country" & keyof T]> extends ReadonlyArray<unknown>
? Populate<NonNullable<T["country" & keyof T]>[number], OmitFirstLevel<Keys, "country">>[]
: NonNullable<Populate<NonNullable<T["country" & keyof T]>, OmitFirstLevel<Keys, "country">>>
}
现在,"country" & keyof T
可能看起来重复和无用,但实际上没有& keyof T
它会抛出K
不能用于索引类型T
的错误。您可以将 & keyof T
视为 K
是 T
的键的断言。所以这进一步简化为:
{
country: NonNullable<T["country"]> extends ReadonlyArray<unknown>
? Populate<NonNullable<T["country"]>[number], OmitFirstLevel<Keys, "country">>[]
: NonNullable<Populate<NonNullable<T["country"]>, OmitFirstLevel<Keys, "country">>>
}
现在看起来还不错,但是到处都是 NonNullable
是怎么回事?因为 T["country"]
是可选的,所以它可以是未定义的。我们不希望在操作它时未定义,所以我们用 NonNullable
排除它。另一种选择是 Exclude<T["country"], undefined>
,但 NonNullable
更短更明确。
为简洁起见,让我们在此处删除 NonNullable
,以便我们了解实际情况:
{
country: T["country"] extends ReadonlyArray<unknown>
? Populate<T["country"][number], OmitFirstLevel<Keys, "country">>[]
: Populate<T["country"], OmitFirstLevel<Keys, "country">>>
}
好的,现在很容易看出发生了什么。
我们检查 T["country"]
是否是一个数组,如果是,我们填充数组元素的类型并将其放回数组中,否则,我们只填充 T["country"]
.
但是用什么键?这就是 OmitFirstLevel
所做的。直到现在我都把它藏起来了,因为它很乱,可以稍微清理一下,但现在是:
type OmitFirstLevel<
Keys,
Target extends string,
R extends ReadonlyArray<unknown> = [],
> = Keys extends readonly [infer First, ...infer Rest]
? SplitPath<First> extends readonly [infer T, ...infer Path]
? T extends Target
? Path extends []
? OmitFirstLevel<Rest, Target, R>
: OmitFirstLevel<Rest, Target, [...R, JoinPath<Path>]>
: OmitFirstLevel<Rest, Target, R>
: OmitFirstLevel<Rest, Target, R>
: R
;
用一些例子可能更容易解释:
OmitFirstLevel<["country", "country.tags"], "country">
给出 ["tags"]
OmitFirstLevel<["country", "country.tags", "country.tags.companies"], "country">
给出 ["tags", "tags.companies"]
OmitFirstLevel<["country", "country.tags", "tags", "tags.companies"], "country">
给出 ["tags"]
您可能会看到它只是获取所有以给定值开头的路径,然后从路径中删除给定值。
现在,进一步简化我们的映射类型:
{
country: T["country"] extends ReadonlyArray<unknown>
? Populate<T["country"][number], ["tags"]>>[]
: Populate<T["country"], ["tags"]>>
}
因为 T["country"]
实际上不是一个数组,所以它简化为:
{
country: Populate<T["country"], ["tags"]>
}
哦,看!它为我们填充了 T["country"]
的 tags
属性!那很好!这就是整盘意大利面的作用。如果您想了解有关 JoinPath
、SplitPath
或 OmitFirstLevel
工作原理的更多详细信息,请提及我,我会修改此 post 以包含一些内容。
我正在尝试创建一个采用 2 个泛型的 Populate
类型:具有可选关系的对象类型(引用其他对象类型的键),以及可以深度填充的路径字符串联合(或者更确切地说,设置为非可选)的关系。例如:有 3 个实体都可以选择相互引用:
type Tag = { name: string, countries?: Country[], companies?: Company[] }
type Company = { name: string, country?: Country, tags?: Tag[] };
type Country = { name: string, companies?: Company[], tags?: Tag[] };
type Populate = { // ... need help here ... // }
type CompanyWithCountry = Populate<Company, 'country'>
type CompanyWithCountryAndTags = Populate<Company, 'country' | 'tags' | 'country.tags'>
type SuperPopulatedCompany = Populate<Company, 'country' | 'country.tags', 'country.tags.companies' | 'tags' | 'tags.companies' | 'tags.countries'>
/** result for SuperPopulatedCompany = {
name: string;
country: null | Populate<Country, 'tags' | 'tags.companies'>, //note for non-array items `null would be possible`
tags: Populate<Tag, 'companies' | 'countries'>[], //for array relations at least an empty array is always returned
}
*/
这样做的目的是让我能够在我的一些查询中使用 TypeORM 的 relations
键键入结果对象,这可以填充相互引用的关系对象。不幸的是,TypeORM 的 return 类型始终是基本实体类型,并且无论您将哪些关系传递到查询中,任何关系始终保持可选。我想转换 return 类型以使关系在被查询时不是可选的。例如:
const company = companyRepository.find({
id: 1,
relations: ['country', 'country.companies', 'country.tags', 'tags', 'tags.companies'],
}) as Populate<Company, 'country' | 'country.companies' | 'country.tags' | 'tags' | 'tags.companies' >
/*
typeof company = {
name: string;
country: null | {
name: string;
companies: Company[], //non-optional
tags: Tags[], //non-optional
}
tags: {
name: string;
companies: Company[], // non-optional
countries?: Country[], //optional (because not in relations query)
}[]
}
Allowing me to access:
company.country,
company.country?.companies,
company.country?.tags,
company.tags,
company.tags[n]?.companies
*/
让我们首先定义两个将要使用的实用程序类型:
type SplitPath<S, R extends unknown[] = []> = S extends `${infer P}.${infer Rest}` ? SplitPath<Rest, [...R, P]> : [...R, S];
type JoinPath<P, S extends string = ""> = P extends [infer First, ...infer Rest] ? JoinPath<Rest, `${S}${S extends "" ? "" : "."}${First & string}`> : S;
这些类型简单地拆分和连接路径(彼此相反)。
SplitPath<"country.tags.companies">
会给我一个元组["country", "tags", "companies"]
JoinPath<["country", "tags", "companies"]>
会给我一个字符串"country.tags.companies"
然后一个类型递归扩展给定类型以进行调试:
// Type to expand results so we can see if it works (mostly for debugging)
// WARNING: BREAKS ON TUPLES
type Expand<T> = T extends ReadonlyArray<unknown> ? Expand<T[number]>[] : T extends object ? { [K in keyof T]: Expand<T[K]> } : T;
没有它,当我们尝试查看 Populate
的结果时,它不会显示结果,而是实际显示 Populate<Company, [...]>
,这没有帮助。使用这种类型,它将 Populate<...>
扩展为我们可以看到的完整形式。
现在我们定义一个简单的find
函数来测试Populate
:
declare function find<Relations extends ReadonlyArray<string> = never>(criteria: {
id?: number;
relations?: Relations;
// ...
}): Expand<Populate<Company, Relations>>;
因为 relations
是可选的,所以我为 Relations
通用参数添加了默认值 never
。然后当你不提供关系时 Populate
没有效果。
填充 Company
后,我们将其展开为完整形式。
这里是 Populate
,乍一看似乎令人生畏:
type Populate<T, Keys extends ReadonlyArray<string>> = Omit<T, SplitPath<Keys[number]>[0]> & {
[K in SplitPath<Keys[number]>[0]]-?:
NonNullable<T[K & keyof T]> extends ReadonlyArray<unknown>
? Populate<NonNullable<T[K & keyof T]>[number], OmitFirstLevel<Keys, K>>[]
: NonNullable<Populate<NonNullable<T[K & keyof T]>, OmitFirstLevel<Keys, K>>>
};
首先,我们忽略 T
中所有受影响的属性,因为我们稍后会添加受影响的属性:
Omit<T, SplitPath<Keys[number]>[0]>
让我们使用一个简单的示例 Populate<Company, ["country", "country.tags"]>
来了解 Populate
的工作原理并简化此 Omit
用法:
Omit<Company, SplitPath<["country", "country.tags"][number]>[0]>
Omit<Company, SplitPath<"country" | "country.tags">[0]>
Omit<Company, (["country"] | ["country", "tags"])[0]>
Omit<Company, "country">
在我们省略所有受影响的 first-level 键之后,我们将其与映射类型相交以将它们添加回来:
{
[K in SplitPath<Keys[number]>[0]]-?:
NonNullable<T[K & keyof T]> extends ReadonlyArray<unknown>
? Populate<NonNullable<T[K & keyof T]>[number], OmitFirstLevel<Keys, K>>[]
: NonNullable<Populate<NonNullable<T[K & keyof T]>, OmitFirstLevel<Keys, K>>>
}
再一次,为了理解我们在这里做什么,让我们简化一下 step-by-step。所以首先像上面一样,我们得到受影响的 first-level 键并映射它们:
{
[K in "country"]-?:
NonNullable<T[K & keyof T]> extends ReadonlyArray<unknown>
? Populate<NonNullable<T[K & keyof T]>[number], OmitFirstLevel<Keys, K>>[]
: NonNullable<Populate<NonNullable<T[K & keyof T]>, OmitFirstLevel<Keys, K>>>
}
-?
要求 属性。没有它,它仍然是可选的。
所以现在,映射类型简化为:
{
country: NonNullable<T["country" & keyof T]> extends ReadonlyArray<unknown>
? Populate<NonNullable<T["country" & keyof T]>[number], OmitFirstLevel<Keys, "country">>[]
: NonNullable<Populate<NonNullable<T["country" & keyof T]>, OmitFirstLevel<Keys, "country">>>
}
现在,"country" & keyof T
可能看起来重复和无用,但实际上没有& keyof T
它会抛出K
不能用于索引类型T
的错误。您可以将 & keyof T
视为 K
是 T
的键的断言。所以这进一步简化为:
{
country: NonNullable<T["country"]> extends ReadonlyArray<unknown>
? Populate<NonNullable<T["country"]>[number], OmitFirstLevel<Keys, "country">>[]
: NonNullable<Populate<NonNullable<T["country"]>, OmitFirstLevel<Keys, "country">>>
}
现在看起来还不错,但是到处都是 NonNullable
是怎么回事?因为 T["country"]
是可选的,所以它可以是未定义的。我们不希望在操作它时未定义,所以我们用 NonNullable
排除它。另一种选择是 Exclude<T["country"], undefined>
,但 NonNullable
更短更明确。
为简洁起见,让我们在此处删除 NonNullable
,以便我们了解实际情况:
{
country: T["country"] extends ReadonlyArray<unknown>
? Populate<T["country"][number], OmitFirstLevel<Keys, "country">>[]
: Populate<T["country"], OmitFirstLevel<Keys, "country">>>
}
好的,现在很容易看出发生了什么。
我们检查 T["country"]
是否是一个数组,如果是,我们填充数组元素的类型并将其放回数组中,否则,我们只填充 T["country"]
.
但是用什么键?这就是 OmitFirstLevel
所做的。直到现在我都把它藏起来了,因为它很乱,可以稍微清理一下,但现在是:
type OmitFirstLevel<
Keys,
Target extends string,
R extends ReadonlyArray<unknown> = [],
> = Keys extends readonly [infer First, ...infer Rest]
? SplitPath<First> extends readonly [infer T, ...infer Path]
? T extends Target
? Path extends []
? OmitFirstLevel<Rest, Target, R>
: OmitFirstLevel<Rest, Target, [...R, JoinPath<Path>]>
: OmitFirstLevel<Rest, Target, R>
: OmitFirstLevel<Rest, Target, R>
: R
;
用一些例子可能更容易解释:
OmitFirstLevel<["country", "country.tags"], "country">
给出["tags"]
OmitFirstLevel<["country", "country.tags", "country.tags.companies"], "country">
给出["tags", "tags.companies"]
OmitFirstLevel<["country", "country.tags", "tags", "tags.companies"], "country">
给出["tags"]
您可能会看到它只是获取所有以给定值开头的路径,然后从路径中删除给定值。
现在,进一步简化我们的映射类型:
{
country: T["country"] extends ReadonlyArray<unknown>
? Populate<T["country"][number], ["tags"]>>[]
: Populate<T["country"], ["tags"]>>
}
因为 T["country"]
实际上不是一个数组,所以它简化为:
{
country: Populate<T["country"], ["tags"]>
}
哦,看!它为我们填充了 T["country"]
的 tags
属性!那很好!这就是整盘意大利面的作用。如果您想了解有关 JoinPath
、SplitPath
或 OmitFirstLevel
工作原理的更多详细信息,请提及我,我会修改此 post 以包含一些内容。