@graphql-codegen/typescript-resolversで互いに依存したプロパティの型定義を出す
はじめに
GraphQL APIのスキーマがある場合,graphql-codegenでTypeScriptの型定義を生成することが出来ることが知られています.
@graphql-codegen/typescript-resolversを食わせるとResolverの型定義が出力されて,これとワチャワチャすると型定義を得られた状態でApollo Serverを作ることが出来ます.これによって,「なんかここ 型的に実装おかしくないですか」という状態などを防ぐことが出来ます[1].ここまではいろんな記事に書いてあり よかったのですが…
困る
例として次のようなGraphQLスキーマを考えてみましょう.Post.createdBy
とUser.posts
の型が互いに依存しています.
type Post {
id: ID!
title: String!
createdBy: User!
}
type User {
id: ID!
name: String!
posts: [Post!]!
}
type Query {
post(id: ID!): Post!
}
このスキーマに対しては例えば次のようなクエリが投げられます
query GetPosts {
post(id: "hoge") {
id
createdBy {
id
name
posts {
id
title
}
}
}
}
Apollo ServerのResolver Chainを考慮すると仮想的にはこのようなResolverを書けばとりあえずは動く筈です[2]
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にわざわざ自分で型を明示するのは面倒なので)という欲求が出てきたとします 実際出来ます 必要なパッケージを入れ 次のように設定を書きます 例えば
schema: ./schema.graphql
generates:
./codegen/resolvers.ts:
plugins:
- typescript
- typescript-resolvers
config:
useIndexSignature: true
しかし…
問題発生
ここで出されたもの(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[];
};
};
型Post
とUser
が互いに循環してしまっています
この型定義に従って実装をすると,例えばQuery.post.createdBy.posts[0].createdBy.posts[0]...
と堂々巡りを実装する必要が発生します.当然そんなこと言われても困りますが,どうすればいいのか?
解決案
ドキュメントに書いてあります( https://www.graphql-code-generator.com/docs/plugins/typescript-resolvers#use-your-model-types-mappers ).mapper
を設定しましょう.
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を加味すればこれで十分な筈です.
export type PostModel = {
id: string;
title: string;
createdBy: {id: string};
};
export type UserModel = {
id: string;
name: string;
posts: {id: string}[];
};
設定を変更した後で生成されるResolverの型定義は./types
で出力される型が利用されておおよそこのような感じになります
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
を設定すればいいぜ という話なので…
この記事はこれを作っている最中で直面した問題についての記事でした.よろしければ見ていってください
おわり
Discussion