[擴大]關於自動生成GraphQL查詢的型別解析
Amplify CodeGen可以根据定义的GraphQL模式自动生成查询。
然而,使用Amplify库的GraphQLAPI执行查询时,返回值类型将变为any。
本文将记录在使用Typescript开发时,如何为自动生成的查询结果加上类型。
如果只想看结论,请直接查看第三步。
如果使用Amplify来执行GraphQL查询
Amplify提供了用于执行GraphQL查询的API。
import API, { graphqlOperation } from '@aws-amplify/api';
API.graphql(graphqlOperation(query,variables));
例如,您可以按照以下方式执行。
import API, { graphqlOperation } from '@aws-amplify/api';
import getPost as queries from '@/graphql/queries'; // 自動生成クエリ
const res = API.graphql(graphqlOperation(getPost,{id:"XXX"}));
/* res
data:{
getPost:{
id: XXX,
title: "XXブログ"
}
}
*/
可以根据上面的方法执行查询,并获得结果,但结果的类型将为any。
当然,如果自定义接口或类型,可以给结果加上类型。
type Post{
id: string;
title: string;
}
然而,在开发过程中我们经常需要重新审视GraphQL模式,每次都要修改类型定义有点麻烦。此外,针对引用其他模型的自动生成查询,由于其层次结构如下所示,所以独立定义所有内容非常困难。
export const getPost = /* GraphQL */
query GetPost($id: ID!) {
getPost(id: $id) {
id
title
comments {
items {
id
content
}
}
}
};
因此,我们将考虑一种可以定义这种层次结构的方法。
第一步(使用API.ts)
执行 CodeGen 会生成类似下面的定义文件(API.ts)的文件。
export type GetPostQueryVariables = {
id: string,
};
export type GetPostQuery = {
getPost: {
__typename: "Post",
id: string,
title: string | null,
comment: {
__typename: "comment",
id: string,
content: string | null,
} | null,
} | null,
};
如果使用这个,似乎可以解决以下的类型问题。
import API, { graphqlOperation } from '@aws-amplify/api';
import getPost as queries from '@/graphql/queries'; // 自動生成クエリ
const res: GetPostQuery = API.graphql(graphqlOperation(getPost,{id:"XXX"})).data;
我认为这样就好了,但有两点我有点在意。
问题:含有__typename的部分。
我觉得有这个可能性会更好,但在类型定义上只是不包含查询执行结果。
import API, { graphqlOperation } from '@aws-amplify/api';
import getPost as queries from '@/graphql/queries'; // 自動生成クエリ
const res: GetPostQuery = API.graphql(graphqlOperation(getPost,{id:"XXX"})).data;
console.log(res.__typename) // undefined
忽略它并不会造成实际伤害,但类型和值之间的差异会使开发有些混乱。
问题:变成可选项的
如果这是本来正确的事情,可能最好不要在意。但是,如果只想创建一个简单的数据类型,可能也有一些需要去除的情况。
import API, { graphqlOperation } from '@aws-amplify/api';
import getPost as queries from '@/graphql/queries'; // 自動生成クエリ
const res: GetPostQuery = API.graphql(graphqlOperation(getPost,{id:"XXX"})).data;
console.log(res.id) // error: res is possibly null
// Optionalなので以下のような実装が必要
if(res) console.log(res.id) //対策1
console.log(res?.id) // 対策2
步骤2(编辑类型定义)
在考虑到上述问题的基础上,以下文章删除了__typename和Optional。
(尽管删除了Optional,但在使用时仍要进行正确的空值检查)
链接:https://medium.com/@dantasfiles/using-typescript-with-aws-amplify-api-3788d722869
import API from '@aws-amplify/api';
type Post= Omit<Exclude<API.GetPostQuery['getPost'], null>,
'__typename'>;
按照顺序解释。
Exclude<T,U> 的作用是从类型 T 中移除可分配给类型 U 的属性。
GetPostQuery[‘getPost’] 是 {…} | null,因此这将变成 {…}。
Omit<T,K> 的作用是从类型 T 中移除属性 K。
因此将从 {…} 中移除属性 __typename。
结果会产生以下的类型。
type Post = {
id: string,
titlte: string | null,
・・・
}
这个看上去就像是简单的数据类型。虽然这样已经可以了,但还有一些令我稍有疑虑的地方。
问题:没有考虑到多个模型合并的情况。
如果有一个数据模型,Post拥有Comment,会发生什么?
type Post = {
id: string,
titlte: string | null,
comments: {
__typename: "ModelCommentConnection",
items: Array<{
__typename: "comment",
id: string,
content: string,
} | null> | null
} | null
}
评论中__typename保持不变,变成可选状态。
步骤3(递归类型定义的编辑)
根据以上内容,我们将进行额外的类型编辑。
思路是将前一步骤递归地应用于每一层。
type Deep<T> =
T extends any[] ? DeepOmitArray<T[number]>:
T extends object ? DeepOmitObject<T>:
T;
type DeepOmitArray<T> = Array<Omit<Exclude<Deep<T>,null>,'__typename'>>
type DeepOmitObject<T> = {
[P in keyof Omit<Exclude<T,null>,'__typename'>]: Deep<Omit<Exclude<T,null>,'__typename'>[P]>;
}
Deep会根据类型T的内容来确定类型。
它分为数组、对象和其他分支。
如果是数组,则为DeepOmitArray类型。
它被表示为Array<Omit<Exclude<Deep, null>,’__typename’>>,它会将数组的内容传递给Deep,并对结果应用第二步的类型编辑。
通过这样做,直到T的内容变为非数组和对象为止,将会递归地进行类型解析。
如果是对象,则为DeepOmitObject类型。
思路与数组相似,对于对象的每个属性,都会将其传递给Deep并进行递归的类型解析。
通过这样做,最终可以得到以下类型。
type Post = {
id: string,
titlte: string | null,
comments: {
items: Array<{
id: string,
content: string,
}>
}
}
我认为在comments下面已经去掉了__typename并使其变为可选项。
最后
在本文中,我們使用了TypeScript Utility Types來解析自动生成GraphQL查询的类型。尽管本次我们删除了Optional类型,但我认为在使用时应该保留。希望能够根据使用方式灵活地进行实现。
由于可能仍然存在不完善的地方,如果有任何指正或建议,我将不胜感激。
另外,在考虑中,我非常感谢以下文章作为参考:
https://medium.com/@dantasfiles/using-typescript-with-aws-amplify-api-3788d722869