我正在使用firestore数据库,并创建了一个模式来建模我的数据:
type FirestoreCollection<T> = {
documentType: T;
subcollections?: {
[key: string]: FirestoreCollection<object>;
};
};
type FirestoreSchema<
T extends { [key: string]: U },
U = FirestoreCollection<object>
> = T;
export type FirestoreModel = FirestoreSchema<{
'chat-messages': {
documentType: ChatMessage;
};
chats: {
documentType: Chat;
subcollections: {
'chat-participants': {
documentType: ChatParticipant;
};
};
};
}>;
这个模型运行良好。
然后我有一个方法可以引用任何集合或子集合,它正确地类型化了我的嵌套关系,看起来像这样:
type GetCollectionArgs<T = FirestoreModel> = {
[K in keyof T]:
| [{ collection: K; id: string }]
| ('subcollections' extends keyof T[K]
? [
{ collection: K; id: string },
...GetCollectionArgs<T[K]['subcollections']>
]
: never);
}[keyof T];
function getDocument(...args: GetCollectionArgs<FirestoreModel>) { ... }
然而,现在我想输入这个函数返回类型,以确保它将正确返回该集合的documentType
。
// should return 'ChatMessage'
getDocument({ collection: 'chat-messages', id: '123' });
// should return 'ChatParticipant'
getDocument(
{ collection: 'chats', id: '123' },
{ collection: 'chat-participants', id: '456' }
);
此外,我希望能够编写一个updateDocument()
方法,以确保第一个参数中的数据是根据GetCollectionArgs
的正确类型:
// included for brevity
type ChatMessage = { message: string }
type ChatParticipant = { name: string }
// this should work
updateDocument(
{ message: 'hello' },
{ collection: 'chat-messages', id: '123' }
)
// and this should too
updateDocument(
{ name: 'ryan' },
{ collection: 'chats', id: '123' },
{ collection: 'chat-participants', id: '456' }
)
// but this should error!
updateDocument(
{ name: 'ryan' },
{ collection: 'chat-messages', id: '123' }
)
有什么想法吗
1条答案
按热度按时间yqlxgs2m1#
我采用的方法是创建一个对象类型的并集,这些对象类型对应于
getDocument()
的允许输入及其相关输出。也就是说,考虑到你的FirestoreModel
,我们将定义以下输入/输出的并集:
然后
getDocument()
可以有以下generic调用签名:其中,我们允许输入为
DocIOFirestoreModel
的i
属性之一,然后使用Extract
实用程序类型选择相应的o
属性。这将给予我们以下可取的行为:您可以通过将返回类型移动到参数类型来编写
updateDocument()
:这给出了以下期望的行为:
既然这一切都按照您希望的方式工作,我们现在的目标是定义一个实用程序类型函数,我们称之为
DocIO<T>
,它采用FireStoreCollection
的某个子类型,定义为并返回
i
/o
类型的并集,因此我们不像上面那样手动写出DocIOFirestoreModel
,而是写它会为我们生成。
这里有一个方法:
这是一个递归定义的 * 分布式对象类型 *(ms/TS#47109中创造的术语),我们通过indexing into a mapped type得到该Map类型中属性的并集。
因此,对于每个键为
K
的属性,我们生成所需的类型作为两个部分的并集。第一个是非递归/基本块{i: [{collection: K, id: string}], o: T[K]['documentType']}
。这意味着每个属性都有一个长度为1的输入,其中collection
是该属性的名称,输出将是该属性的documentType
。第二个是递归部分,上面写为三个嵌套的conditional types。看起来第一个条件类型()将
subcollections
属性提取到新的类型参数F
中。如果当前属性没有subcollections
属性,它将变为never
,递归到此停止。第二个条件类型()通过计算
DocIO<F>
来执行实际的递归步骤;这被复制到一个新的类型参数IOF
中,用于以下目的:第三个条件类型()是一个分配条件类型,它分别对
IOF
的每个联合成员进行操作,然后将结果组合成一个新的联合。我想这样做,以便我们将子树的i
/o
类型彼此分开。它还将每个联合成员的i
和o
类型分别提取到新的类型参数I
和O
中。最后是实际返回的类型,
o
类型与O
类型相同,但其i
类型是当前路径,{collection: K, id: string}
前置到I
类型的旧i
路径上。这就是它,让我们来测试一下:
看起来不错,这就是我们想要的!
Playground链接到代码