让我们首先定义将使用的两种实用程序类型:
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>>;
Because relations
是可选的我添加了默认值never
为了Relations
通用参数。然后当你不提供关系时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
usage:
Omit<Company, SplitPath<["country", "country.tags"][number]>[0]>
Omit<Company, SplitPath<"country" | "country.tags">[0]>
Omit<Company, (["country"] | ["country", "tags"])[0]>
Omit<Company, "country">
在我们省略所有受影响的第一级键之后,我们将其与映射类型相交以将它们添加回来:
{
[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>>>
}
再次强调,为了理解我们在这里所做的事情,让我们逐步简化这一过程。因此,首先像上面一样,我们获取受影响的一级键并映射它们:
{
[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">>>
}
Now, "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>
but 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">
gives ["tags"]
-
OmitFirstLevel<["country", "country.tags", "country.tags.companies"], "country">
gives ["tags", "tags.companies"]
-
OmitFirstLevel<["country", "country.tags", "tags", "tags.companies"], "country">
gives ["tags"]
您可能会看到它只是获取以给定值开头的所有路径,然后从路径中删除给定值。
现在,为了进一步简化我们的映射类型:
{
country: T["country"] extends ReadonlyArray<unknown>
? Populate<T["country"][number], ["tags"]>>[]
: Populate<T["country"], ["tags"]>>
}
Because T["country"]
实际上不是一个数组,它简化为:
{
country: Populate<T["country"], ["tags"]>
}
哦,看!它填充T["country"]
's tags
属于我们的财产!那很好!这就是整盘意大利面的作用。如果您想了解更多详细信息JoinPath
, SplitPath
, or OmitFirstLevel
有效,提及我,我将修改这篇文章以包含一些内容。
操场