[擴大]關於自動生成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