🥰
AppSyncでのTypeScriptの型生成にはtyped-document-nodesを使おう
概要
- AppSyncをTypeScriptで利用するときに、graphql-code-generator を使って型を作成していました。
- queryをするときに、引数の型と戻り値の型の両方を渡す必要があり、コードが長くなっていました。例はシンプルなのでまだそんなに見にくくはないですが。
sample.ts
const queryRates = async () => {
const result = await query<RatesQueryVariables, RatesQuery>(rates, {
currency: "USD"
});
const currency = result.rates[0].rate;
};
- typed-document-nodesのプラグインを利用することで、graphqlの情報を渡すだけで型を定義する必要なく簡単になります。
typed-document-node.ts
const queryRates = async () => {
const result = await query(RatesDocument, {
currency: "USD"
});
const currency = result.rates[0].rate;
};
環境
- AWS AppSync
- TypeScript
使い方
-
GraphQL Code Generatorが導入されている前提で進めていきます。
-
導入の方法は以下のリンクを参考にしてください
-
公式のsampleを使いながら説明していきます。
-
package.jsonに追加後、pluginsにtyped-document-nodeを追加します。
npm install -D @graphql-codegen/typed-document-node
codegen.yml
schema: SCHEMA_FILE_OR_ENDPOINT_HERE
documents: "./src/**/*.graphql"
generates:
./src/graphql-operations.ts:
plugins:
- typescript
- typescript-operations
- typed-document-node
- graphql-codegenを実行するとxxxDocumentというtypeが生成されます。
- xxxDocumentをgql-tagで作成するqueryの代わりに利用します。
graphql-operations.ts
// Represents the variables type of the operation - generated by `typescript` + `typescript-operations` plugins
export type RatesQueryVariables = Exact<{
currency: Scalars['String'];
}>;
// Represents the result type of the operation - generated by `typescript` + `typescript-operations` plugins
export type RatesQuery = (
{ __typename?: 'Query' }
& { rates?: Maybe<Array<Maybe<(
{ __typename?: 'ExchangeRate' }
& Pick<ExchangeRate, 'currency' | 'rate'>
)>>> }
);
// Generated by this plugin - creates a pre-compiled `DocumentNode` and passes result type and variables type as generics
export const RatesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"rates"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"currency"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"rates"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"currency"},"value":{"kind":"Variable","name":{"kind":"Name","value":"currency"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"currency"}},{"kind":"Field","name":{"kind":"Name","value":"rate"}}]}}]}}]} as unknown as DocumentNode<RatesQuery, RatesQueryVariables>;
- queryの実行は以下のように定義しています。単純にAppSyncClientを生成しているだけです。
import { TypedDocumentNode } from "@apollo/client";
import { AWSAppSyncClient } from "aws-appsync";
const client = new AWSAppSyncClient({
// 省略
});
export const query = async <V, R>(
query: TypedDocumentNode<R, V>,
variables: V
): Promise<R> => {
try {
const res = await (await client.hydrated()).query({
query: query,
fetchPolicy: "network-only",
variables
});
if (res.errors) {
console.log(res.errors);
throw res.errors;
}
return res.data as R;
} catch (e) {
console.log(e);
throw e;
}
};
- 以下のように利用できるようになります
index.ts
import { RatesDocument } from "./typed-document-nodes";
import { query } from "./gql";
const queryTypedDocumentNodes = async () => {
const result = await query(RatesDocument, {
currency: "USD"
});
const currency = result.rates[0].rate;
};
typed-document-nodesで生成されているのは何か
- graphql-tagではqueryからASTを生成しています。
- https://github.com/apollographql/graphql-tag
sample.ts
import gql from 'graphql-tag';
const query = gql`
{
user(id: 5) {
firstName
lastName
}
}
`
- これは以下のjsonを生成するようになっています。
{
"kind": "Document",
"definitions": [
{
"kind": "OperationDefinition",
"operation": "query",
"name": null,
"variableDefinitions": null,
"directives": [],
"selectionSet": {
"kind": "SelectionSet",
"selections": [
{
"kind": "Field",
"alias": null,
"name": {
"kind": "Name",
"value": "user",
...
}
}
]
}
}
]
}
- typed-document-nodeでは上記のASTをcodegen時に作成しています。
サンプル
- codesandboxに前後の比較書いています。
https://codesandbox.io/s/gracious-jackson-23hno?file=/index.ts
メリット
コードがすっきりする
- 概要の例でも記載しましたが、型を指定する部分が必要なくなるので、コードがすっきりします。
- 個人的には、どっちがVariableでどっちがQueryなのか毎回考えながら書いていたので結構楽になった印象です。
fragmentの管理が楽になる
- gql-tagを利用した場合、fragmentを利用したtsファイルを作成する場合に都度importが必要になります。
- そのため、どのfragmentがどのソースに書いてあったかをちゃんと把握する必要があります。
- typed-document-nodesを利用した場合、すべてのqueryやfragmentは一つのファイルに出力されます。そのため、どこにあるのか気にすることなく利用できます。
デメリット
すべてのクエリがグローバルになるため、名前空間に工夫が必要になる
- gql-tagを利用した場合は必要なファイルのみをimportすればよかったので、最悪query名が重複しても問題ありませんでした。
- typed-document-nodesを利用するとすべてのqueryは同じファイルに出力するため、queryの命名規則を決めないと重複してしまう恐れがあります。
- また、意図しない部分から呼ばれてしまって変更に弱くなるようなこともあるかもしれません。
参考リンク
- https://the-guild.dev/blog/typed-document-node
- https://blog.sinki.cc/entry/2020/08/18/190426
- https://qiita.com/shinnoki/items/576277184de0e89cab97
メモ
- AppSyncClientだからuseQueryが使えないって思っていたら、AppSyncClientはApolloClientをextendsしているので、そのまま使えるっぽいですね。今度試してみます。
aws-appsync/lib/client.d.ts
declare class AWSAppSyncClient<TCacheShape extends NormalizedCacheObject> extends ApolloClient<TCacheShape> {
private _store;
private hydratedPromise;
hydrated(): Promise<AWSAppSyncClient<TCacheShape>>;
private _disableOffline;
constructor({ url, region, auth, conflictResolver, complexObjectsCredentials, cacheOptions, disableOffline, offlineConfig: { storage, keyPrefix, callback, }, }: AWSAppSyncClientOptions, options?: Partial<ApolloClientOptions<TCacheShape>>);
isOfflineEnabled(): boolean;
mutate<T, TVariables = OperationVariables>(options: MutationOptions<T, TVariables>): Promise<import("apollo-link").FetchResult<T, Record<string, any>, Record<string, any>>>;
sync<T, TVariables = OperationVariables>(options: SubscribeWithSyncOptions<T, TVariables>): Subscription;
}
Discussion