我正在尝试用TypeScript为图形数据库写一个ORM。具体来说,就是“find”方法,它将返回一个特定实体的列表。现在,还可以向该函数传递一个与应该在数据库级别上完成的连接相关的结构。理想情况下,函数应该自动输入这些附加字段,以便客户端可以访问它们。只有一个层次的嵌套,我已经能够完成这一点,虽然这将是可怕的,使它与多个层次的工作。
我的(已经工作的)解决方案只有一个嵌套级别如下:
interface IDocumentModel {
_id?: string;
}
type JoinParams<T extends Record<string, IDocumentModel>> = {
[K in keyof T]: {
model: DocumentModel<T[K]>;
};
};
type JoinResult<T, U> = (U & {
[K in keyof T]: T[K][];
})[];
class DocumentModel<T extends IDocumentModel> {
async find<X extends Record<string, IDocumentModel>>(
filter?: Partial<T>,
hydrate?: JoinParams<X>,
): Promise<JoinResult<X, T>> {
// TODO: implementation
}
}
const ParentModel = new DocumentModel<{ _id?: string, parentField: string }>();
const ChildModel = new DocumentModel<{ _id?: string, childField: string }>();
const results = await ParentModel.find(
{ _id: 'abc' },
{
children: {
model: ChildModel,
},
},
);
console.log(results[0].parentField);
console.log(results[0].children[0].childField);
现在的挑战是将其扩展到两个级别,甚至更好的是任意数量的级别。这是我正在进行的两层嵌套的工作:
以下是我目前围绕这个问题的解决方案。
interface IDocumentModel {
_id?: string;
}
type JoinParams<
T extends
| Record<string, IDocumentModel>
| Record<string, Record<string, IDocumentModel>>,
> = {
[K in keyof T]: {
model: T extends Record<string, Record<string, IDocumentModel>>
? DocumentModel<T[K]['parent']>
: T extends Record<string, IDocumentModel>
? DocumentModel<T[K]>
: never;
hydrate?: T extends Record<string, Record<string, IDocumentModel>>
? JoinParams<Omit<T[K], 'parent'>>
: never;
};
};
type JoinResult<
T extends
| Record<string, IDocumentModel>
| Record<string, Record<string, IDocumentModel>>,
U,
> = (U & {
[K in keyof T]: T extends Record<string, Record<string, IDocumentModel>>
? JoinResult<Omit<T[K], 'parent'>, T[K]['parent']>
: T extends Record<string, IDocumentModel>
? T[K][]
: never;
})[];
class DocumentModel<T extends IDocumentModel> {
async find<X extends Record<string, Record<string, IDocumentModel>>>(
filter?: Partial<T>,
hydrate?: JoinParams<X>,
): Promise<JoinResult<X, T>> {
// TODO: implementation
}
}
const ParentModel = new DocumentModel<{ _id?: string, parentField: string }>();
const ChildModel = new DocumentModel<{ _id?: string, childField: string }>();
const GrandChildModel = new DocumentModel<{ _id?: string, grandChildField: string }>();
const results = await ParentModel.find(
{ _id: 'abc' },
{
children: {
model: ChildModel,
hydrate: {
grandchildren: {
model: GrandChildModel,
},
},
},
},
);
console.log(results[0].parentField);
console.log(results[0].children[0].childField);
console.log(results[0].children[0].grandchildren[0].grandChildField);
当我尝试我的测试案例时
console.log(results[0].parentField);
console.log(results[0].children[0].childField);
console.log(results[0].children[0].grandchildren[0].grandChildField);
我没有得到任何超过results[0].parentField
的自动完成。这意味着IDE不再建议将results[0].children
作为有效字段。
我希望这是足够的信息,虽然我很乐意澄清更多,如果不清楚。
1条答案
按热度按时间omqzjyyz1#
当
JoinParams
是递归conditional type时,TypeScript不够聪明,无法从JoinParams<T>
类型的hydrate
值推断generic类型参数T
。类型函数F<T>
越复杂,编译器就越不可能从中推断出T
。您应该重构hydrate
参数,使其类型与您试图推断的泛型类型参数非常简单地相关,而不是尝试这样做。最简单的关系是身份:如果你想从hydrate
中推断出H
,那么直接将hydrate
的类型设为H
。然后你可以从H
* 计算 * 你的其他类型。一种方法看起来像:
在这里,我们将
H
约束为Hydrate
,这种类型应该允许有效值而不允许无效值。Hydrate
是按照SubHydrate
编写的,SubHydrate
本身在模型类型M
和嵌套的Hydrate
类型H
中是通用的。所以H
应该很容易从对find()
的调用中推断出来。然后
find()
的返回类型是Find<M, H>
,它完成了将hydrate
和model
的类型转换为预期输出类型的工作。它通过H
递归下降,从它推断嵌套的M
和H
类型。让我们看看它的实际操作:
看上去不错。一切都如你所愿。
Playground链接到代码