GraphQL(Apollo Client)をTypeScriptで利用する際に、TypeScriptの型定義と
Schemaの定義とで二重に定義を記述する手間を省くため、Apollo CLI(apollo-tooling)を利用してSchemaからTypeScriptの型定義を自動生成させます。
The Apollo CLI - APOLLO DOCS
Apollo CLI - Github
名称は「Apollo CLI」の様ですが、Githubのリポジトリ名は「apollo-tooling」、npm上では「apollo」で、また「Apollo Client」とも似ていて少々紛らわしいです。
本記事内では、「apollo-tooling」の名称を用いることにします。
apollo-tooling(Apollo CLI) のインストール
apollo-tooling自体は、Apolloで提供されている各種ライブラリをコマンドライン上で操作することを可能にするためのツールです。
Error: Cannot use GraphQLSchema "[object GraphQLSchema]" from another module or realm.
既にdependenciesの方で"graphql": "^14.5.8"
をインストールしている状態で、
追加で"apollo": "^2.23.0"
をインストールしたところ、後にapollo-toolingのコマンドを実行する際に
下記エラーに遭遇しました。
Error: Cannot use GraphQLSchema "[object GraphQLSchema]" from another module or realm.
Ensure that there is only one instance of "graphql" in the node_modules
directory. If different versions of "graphql" are the dependencies of other
relied on modules, use "resolutions" to ensure only one version is installed.
https://yarnpkg.com/en/docs/selective-version-resolutions
Duplicate "graphql" modules cannot be used at the same time since different
versions may have different capabilities and behavior. The data from one
version used in the function from another could produce confusing and
spurious results.
node_modules > apollo > package.json を確認したところ、dependenciesに"graphql": "14.0.2 - 14.2.0 || ^14.3.1"
の記載があり、
node_modules > apollo > node_modules 配下にgraphqlフォルダがありました。
これが、既存のものと衝突してしまっていたようです。
エラーメッセージにある「(yarnの)resolutionsを使え」という指示に従い、package.jsonに下記項目を追加して node_modules を入れなおしたところ、無事エラーが解消されました。
同様のエラーが発生する可能性がある場合は、インストールを実行する前に予め追記しておくことをお勧めします。
npmをご利用の場合は、直接バージョンを書き換えることになりそうです。(筆者は実際に試してはいません。)
https://github.com/apollographql/apollo-tooling/issues/1296#issuecomment-497862004
"resolutions": {
"graphql": "^14.5.8",
"graphql-tag": "^2.10.1"
},
// graphql-tagの重複エラーは見かけませんでしたが、重複インストールは発生していたのでついでに追加しました。
エラー回避の下準備が完了したら、下記コマンドでインストールします。
yarn add -D apollo
# OR
npm install -D apollo
apollo.config.js の準備
公式サイトの説明に従って、プロジェクトのルートディレクトリに「apollo.config.js」を準備します。
Configuring Apollo projects
今回は、リモートエンドポイントからSchema情報を取得します。
module.exports = {
client: {
name: 'client',
includes: ['src/**/*.ts'],
tagName: 'gql',
addTypename: true,
service: {
// remote endpoint
name: 'sever',
url: 'https://server/graphql',
headers: {
authorization: 'nanikashiranohituyounatoken...',
},
},
},
};
tagName
はGraphQLクエリを記述したテンプレートリテラルを引き渡す関数名です。(デフォルト値gql
)
上記ではsrc
配下の.ts
ファイル内で、gql
関数の引数値にあるテンプレートリテラルを参照しろという指示になっています。
また、addTypename
にて型定義ファイル生成の際にSchemaで定義されているType名を追記するとしています。(デフォルト値true
)
client.service
にて、参照先のサーバを指定しています。
型定義生成実行
package.jsonにscriptを追加して実行します。
オプションが色々と用意されています。
https://github.com/apollographql/apollo-tooling#apollo-clientcodegen-output
型定義出力のコマンドはapollo client:codegen [OPTIONS] [OUTPUT]
です。
[OUTPUT]
は出力ファイルのパスです。特に指定しない場合、元ファイルと同じディレクトリ内に__generated__
というフォルダを生成し、クエリ毎に定義ファイルを出力します。
この際、クエリの名前が重複しているとエラーになります。
"scripts": {
"apollo:codegen": "apollo client:codegen --target=typescript --globalTypesFile=src/types/globalTypes.ts"
}
typescript以外にも、swift, flow, scala に対応している為、--target=typescript
で明記します。
共通で利用する型定義ファイル(globalTypesFile
)が、そのままだとルートフォルダ上(/__generated__/globalTypes.ts
)に生成された為、/src/types/
配下に指定しています。
予めSchemaファイルをダウンロードしておく
サーバアクセス時のトークンの期限が短い等々毎回エンドポイント参照が面倒な場合は、事前にローカルにダウンロードしておきます。
"scripts": {
"apollo:download": "apollo client:download-schema src/type/schema.json",
"apollo:codegen": "apollo client:codegen --target=typescript --localSchemaFile=src/type/schema.json"
}
apollo:download
でダウンロードしてsrc/type/schema.json
に保存し、apollo:codegen
実行時にはこのファイルを参照させます。
Custom Scalarの対応
日付などの独自に定義した型(Custom Scalar)はデフォルトでany
になります。
apollo:codegen
のオプションに--passthroughCustomScalars
を指定すると、Custom Scalarの名前がそのまま出力されます。
ただ、該当する型がglobalTypesFileで定義さることはないので、予め自分で用意しておく必要があります。
// 日付型の Custom Scalar の名前が GqlDate の場合
declare global {
type GqlDate = Date;
}
また、実際に受け取ったデータがDate型に変換されるわけではないので、別途データ型変換処理を実施する必要はあります。
// 「GqlDate」が、「ISO 8601」形式の文字列型で送信されるとした場合
// GqlDate(ISO 8601)判定
export const isGqlDate = (arg: any): arg is GqlDate => {
return (
typeof arg === 'string' &&
/^\d{4}(-\d\d){2}T\d{2}(:\d\d){2}.\d{2,3}Z$/.test(arg)
);
};
// GqlDate文字列をDateオブジェクトに変換
export const convertGqlDateToDate = <T extends Object = {}>(arg: T): T => {
let res: { [key: string]: any } = {};
for (const [key, value] of Object.entries(arg)) {
if (isGqlDate(value)) {
res[key] = new Date(value);
} else if (Object.prototype.toString.call(value) === '[object Object]') {
res[key] = convertGqlDateToDate(value);
} else if (Array.isArray(value)) {
// GqlDate[] には対応していない……
res[key] = value.map(item => {
if (Object.prototype.toString.call(value) === '[object Object]') {
return convertGqlDateToDate(item);
}
return item;
});
} else {
res[key] = value;
}
}
return res as T;
};
// DateオブジェクトをGqlDate(ISO 8601)に変換
export const convertDateToGqlDate = <T extends Object = {}>(arg: T): T => {
let res: { [key: string]: any } = {};
// ...略
return res as T;
};
// variablesとonCompletedで変換処理を挟む
const [state, setState] = useState<StateType | null>(null);
const {loading, error} = useQuery<ResultType, VariablesType>(
ANY_QUERY,
{
variables: convertDateToGqlDate<VariablesType>(anyData),
onCompleted(result) {
setState(
convertGqlDateToDate<StateType>(result.something)
);
},
},
);
Apollo Client側でのCustom Scalarの導入に関しては、ライブラリを作成している方もいらっしゃるようです。
正式導入まだかな。。。
参考情報
Automatically Generate TypeScript Definitions for GraphQL Queries with Apollo Codegen
TypeScript + Apollo ClientでGraphQLのデータに型を付ける
Apollo toolingでTypeScriptのクライアントの型定義を自動生成