⛓️

@graphql-codegen/typescript-resolversで互いに依存したプロパティの型定義を出す

2021/09/24に公開

はじめに

GraphQL APIのスキーマがある場合,graphql-codegenでTypeScriptの型定義を生成することが出来ることが知られています.

@graphql-codegen/typescript-resolversを食わせるとResolverの型定義が出力されて,これとワチャワチャすると型定義を得られた状態でApollo Serverを作ることが出来ます.これによって,「なんかここ 型的に実装おかしくないですか」という状態などを防ぐことが出来ます[1].ここまではいろんな記事に書いてあり よかったのですが…

困る

例として次のようなGraphQLスキーマを考えてみましょう.Post.createdByUser.postsの型が互いに依存しています.

./schema.graphql
type Post {
  id: ID!
  title: String!
  createdBy: User!
}

type User {
  id: ID!
  name: String!
  posts: [Post!]!
}

type Query {
  post(id: ID!): Post!
}

このスキーマに対しては例えば次のようなクエリが投げられます

GetPosts.graphql
query GetPosts {
  post(id: "hoge") {
    id
    createdBy {
      id
      name
      posts {
        id
	title
      }
    }
  }
}

Apollo ServerのResolver Chainを考慮すると仮想的にはこのようなResolverを書けばとりあえずは動く筈です[2]

server.ts
type Post = {id: string; title: string; createdBy: {id: string};}
type User = {id: string; name: string; posts: {id: string}[];}

type getPost = (postId: string) => Post
type getPosts = (postIds: {id: string}[]) => Post[]
type getUser = (userId: string) => User

const resolver = {
  Query: {
    post(parent, args: {id: string}) {
      return getPost(id)
    }
  },
  Post: {
    createdBy(parent: Post) {
      return getUser(id)
    }
  },
  User: {
    posts(parent: User) {
      return getPosts(parent.posts) // この辺りとかそんな雑な実装無いだろとは思いますが まあとりあえず
    }
  }
}

ここまではcodegenが登場せずに自力で型を与えていますが,ここでresolverの型をcodegenで生成したい(parentとかargsにわざわざ自分で型を明示するのは面倒なので)という欲求が出てきたとします 実際出来ます 必要なパッケージを入れ 次のように設定を書きます 例えば

./codegen.yml
schema: ./schema.graphql

generates:
  ./codegen/resolvers.ts:
    plugins:
      - typescript
      - typescript-resolvers
    config:
      useIndexSignature: true

しかし…

問題発生

ここで出されたもの(codegen/resolvers.ts)を素朴に使うと問題が発生します

自動生成される型定義をバーンと載せてしまうと複雑で長いので要役しますが,ザックリ言えば次のような状態になっています.

./codegen/resolvers.ts
type Post = {
  id: string;
  title: string;
  createdBy: User;
};

type User = {
  id: string;
  name: string;
  posts: Post[];
};

type Resolver = {
  Query: {
    post(parent: {}, args: {id: string}): Post;
  };
  Post: {
    createdBy(parent: Post): User;
  };
  User: {
    posts(parent: User): Post[];
  };
};

PostUserが互いに循環してしまっています

この型定義に従って実装をすると,例えばQuery.post.createdBy.posts[0].createdBy.posts[0]...と堂々巡りを実装する必要が発生します.当然そんなこと言われても困りますが,どうすればいいのか?

解決案

ドキュメントに書いてあります( https://www.graphql-code-generator.com/docs/plugins/typescript-resolvers#use-your-model-types-mappers ).mapperを設定しましょう.

codegen.yml
schema: ./schema.graphql

generates:
  ./codegen/resolvers.ts:
    plugins:
      - typescript
      - typescript-resolvers
    config:
      useIndexSignature: true
      mappers:
        Post: ./types#PostModel
        User: ./types#UserModel

この設定では,./codegen/resolvers.ts生成先のディレクトリからの./types.ts,つまり./codegen/types.tsからexportされているPostModelを参照することを伝えています.(codegen.ymlを起点にすることではないことに注意)

さて,./codegen/types.tsですが 例えばこんな感じでお互いに依存しない形で定義してみましょう.ApolloのResolver Chainを加味すればこれで十分な筈です.

./codegen/types.ts
export type PostModel = {
  id: string;
  title: string;
  createdBy: {id: string};
};

export type UserModel = {
  id: string;
  name: string;
  posts: {id: string}[];
};

設定を変更した後で生成されるResolverの型定義は./typesで出力される型が利用されておおよそこのような感じになります

./codegen/resolvers.ts
import {PostModel, UserModel} from "./types"

type Resolver = {
  Query: {
    post(parent: {}, args: {id: string}): PostModel;
  };
  Post: {
    createdBy(parent: Post): UserModel;
  };
  User: {
    posts(parent: User): PostModel[];
  };
};

こうすることで,Query.post.createdByで適切に切れるので よかったよかったということになります

あとはサーバー実装でこれを使ってガーッと書いていくといった流れになります

おわりに

他にも「Contextの型anyなんだが どうするんだよ」などの問題も解決できます 詳しくはドキュメントを読んでください

この記事に書かれているコード及びその他色々は,多分合っていると信じていますが,空で書いているので全然間違っているという可能性もあります が 要点はmappersを設定すればいいぜ という話なので…

この記事はこれを作っている最中で直面した問題についての記事でした.よろしければ見ていってください

おわり

脚注
  1. あと単純にコード補完が効くので気持ちよくコードが書けるといった点もあります ↩︎

  2. 今回はデータの取得部分についてはなんでもいいので実装は書かず型だけ示しておきます ↩︎

Discussion