我如何得到一个深键与点访问路径,允许循环引用



我正在尝试创建一个通用类型,它接受一个对象,并返回其所有嵌套键的路径字符串的联合,这些键是对象或对象数组。

例如:

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 RelationsOf<T extends object> = { // ...need help // }
type CompanyRelations = RelationsOf<Company> // "country" | "country.companies" | "tags" | "tags.countries" | etc (up to depth limit)
type CountryRelations = RelationsOf<Country> // "companies" | "companies.country" | "tags" | "tags.companies" | etc (up to depth limit)
// ... elsewhere ///
type Populated<T extends object, Relations extends RelationsOf<T> = { // ... different problem, but shows intention // }
type SuperPopulatedCompany = Populated<Company, "country" | "country.companies" | "country.companies.tags" | "tags" | "tags.company" | etc (depth limit) >
/** 
{
name: string;
country: null | Populated<Country, "companies" | "companies.tags">
tags: Populated<Tag, "company">[]
}

由于循环引用,我所有的尝试都遇到了像"表达式太深,可能是无限的"这样的错误。如果可以增加深度计数器,那么将深度限制设置为3就可以了。这样做的目的是与TypeORM一起使用,以便当我在实体上查询relations时,我可以将结果转换为填充了这些关系的实体类型,而不是所有可选的内容(它不会立即这样做,而且我找不到任何关于如何实现这一点的提示)。所以它最终会被这样的东西使用:

const getCompany = <T extends RelationsOf<Company>[]>(id: string, relations: T) => {
return companyRepository.find({
id, 
relations, // eg ['countries', 'tags', 'tags.companies'] 
}) as Populated<Company, T[number]>
}

// ... elsewhere //
getCompany(['countries', 'tags', 'tags.companies']) // returns Populated<Company, "countries" | "tags" | "tags.companies">

您的RelationsOf<T>在精神上与Paths<T>相似,正如对类似问题的回答所描述的那样。像Paths<T>一样,这种深度嵌套的类型是棘手和脆弱的,并且很容易导致编译器抱怨实例化深度(如果幸运的话),或者完全陷入困境和无响应(如果不幸的话)。对于任何递归类型,通过对象类型的潜在路径的数量在路径深度上趋向于指数。因此,深度限制是一个好主意,尽管有时即使深度限制也会对编译器性能产生问题。

这是实现RelationsOf<T, D>的一种方法,其中D是一个可选的数字文字类型,对应于所需的深度(如果您不指定D,则默认为3):

type RelationsOf<T extends object, D extends number = 3> =
[D] extends [0] ? never :
T extends object[] ? RelationsOf<T[number], D> :
keyof T extends infer K ?
K extends string & keyof T ?
T[K] extends object ?
K | `${K}.${RelationsOf<T[K], Prev[D]>}` :
never : never : never;
type Prev = [never, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];

Prev类型是一个帮助器,以便编译器可以轻松地从您提供的D中减去一个(例如,Prev[10]9);它目前停在15。你可以写得更长一些,但是你可能会遇到深度问题。

它的工作方式:计算RelationsOf<T, D>我们首先检查D是否为0。如果是这样,我们就退出。否则,我们检查T是否是一个对象数组。如果是,则计算RelationsOf<T[number], D>,其中T[number]T数组的元素类型。(我们这样做是为了跳过数组键对应的一层深度;否则你最终会得到像`tags.${number}.companies`而不是"tags.companies"这样的路径。)

在这里,我们使用分配条件类型来获取keyof T,并对每个字符串联合成员K执行类型操作。因此,如果keyof T"name" | "country" | "tags",那么K将依次是"name""country""tags",并且类型操作的结果将连接在一个联合中。

类型操作是:如果TK键处的属性类型(T[K])是一个对象,则计算K | `${K}.${RelationsOf<T[K], Prev[D]>}`。也就是说,我们直接使用键本身,并将它(和一个点)添加到RelationsOf操作属性类型的结果中,深度减少1。如果T[K]不是对象,则使用never,因为我们不希望在输出类型中出现该键。


让我们测试一下:

type CompanyRelations = RelationsOf<Company>;
/* type CompanyRelations = "country" | "tags" | "tags.companies" | 
"tags.countries" | "country.tags" | "country.companies" | 
"country.tags.companies" | "country.tags.countries" | 
"country.companies.country" | "country.companies.tags" | 
"tags.companies.country" | "tags.companies.tags" | 
"tags.countries.tags" | "tags.countries.companies" */
type CountryRelations = RelationsOf<Country>;
/* type CountryRelations = "companies" | "tags" | "tags.companies" | 
"tags.countries" | "companies.tags" |   "companies.country" | 
"companies.tags.companies" | "companies.tags.countries" | 
"companies.country.companies" | "companies.country.tags" | 
"tags.companies.tags" |   "tags.companies.country" | 
"tags.countries.companies" | "tags.countries.tags" */

看起来是对的。每条路径的长度都不超过3。如果我们增加D,我们看到联合变长了(路径也变长了):

type CompanyRelations4 = RelationsOf<Company, 4>
/* type CompanyRelations4 = "country" | "tags" | "tags.companies" | 
"tags.countries" | "tags.companies.country" | "tags.companies.tags" | 
"tags.countries.tags" | "tags.countries.companies" | "country.tags" |
"country.companies" | "country.companies.country" | 
"country.companies.tags" | "country.tags.companies" | 
"country.tags.countries" | "country.tags.companies.country" | 
"country.tags.companies.tags" | "country.tags.countries.tags" | 
"country.tags.countries.companies" | "country.companies.tags.companies" | 
"country.companies.tags.countries" | "country.companies.country.tags" | 
"country.companies.country.companies" | "tags.companies.tags.companies" | 
"tags.companies.tags.countries" | "tags.companies.country.tags" | 
"tags.companies.country.companies" | "tags.countries.companies.country" | 
"tags.countries.companies.tags" | "tags.countries.tags.companies" | 
"tags.countries.tags.countries" */

如果我们尝试10,编译器甚至不会单独列出它们,因为有超过2000个条目;

type CompanyRelations10 = RelationsOf<Company, 10>;
/* type CompanyRelations10 = "country" | "tags" | "tags.companies" | 
"tags.countries" | "tags.companies.country" | "tags.companies.tags" | 
"tags.countries.tags" | "tags.countries.companies" | "country.tags" | 
... 2036 more ... | 
"tags.countries.tags.countries.companies.country.companies.country.tags.countries" 
*/

好的,看起来不错。

Playground链接到代码

最新更新