TypescriptでGraphQL Code Generatorから自動生成されるクエリ宣言から部分型を抽出する


※ 当ページには【広告/PR】を含む場合があります。
2024/09/14
TypesriptでもBitwiseなテンプレートリテラル型で簡単ビット演算チェック
蛸壺の技術ブログ|TypescriptでGraphQL Code Generatorから自動生成されるクエリ宣言から部分型を抽出する

GraphQLを使う際に、スキーマに従って自動で型定義を行ってくれる
「GraphQL Code Generator(graphql-codegen)」はとても便利です。

スキーマの構造にも依りますが、実際に生成された型定義は、例えば、以下のようなクエリを定義して、

            
            query GetHoge {
  hoge {
    piyo {
        fuga {
            mofu
        }
    }
  }
}
        
このクエリに対応する型定義をgraphql-codegenから生成してみるとおおよそ以下のようなものが生成されます。

            
            type GetHogeQuery = {
  __typename?: 'Query';
  hoge: {
    __typename?: 'Hoge';
    piyo: Array<{
        __typename?: 'Piyo';
        fuga: Array<{
            __typename?: 'Fuga';
            mofu: string;
        }>
    }>
  }
}
        
ここから見て取れるように、graphql-codegenの生成する型は総じて、オブジェクトリテラル型{__typename?: string}の部分型T extends {__typename?: string}か、その配列型(T extends {__typename?: string})[]のUnion型が入れ子構造を形成しています。

やろうと思えば、かなり深くまで同型をネストさせることができるわけですが、深くなればなるほど、どんな型が定義されているのか分からなくなってしまうのが難点です。

最近、同じような問題に取り組まれた方の記事を拝見する機会がありました。

参考|TypeScriptの型再帰呼び出しを使って、graphql-codegenで自動生成された型から部分的な型を取り出す

2番煎じ感はありますが、補足説明として著者流にこの問題を扱ってみたいと思います。


合同会社タコスキングダム|蛸壺の技術ブログJavascript(js)&Typescript(ts)プログラミング入門〜これから学ぶ人のためのおすすめ書籍&教材の手引き

自動生成したGraphQLスキーマ型の部分型を取り出す手順

少しずつ小出しに型を試して目的に近づけていく手法をとりましょう。

まず、以下のような
extractTypeNameのようなプロトタイプの型をこの段階では再帰的な処理なく、手動でネストしたオブジェクト型を紐解いていく実験をしてみます。

            
            type extractTypeName<T, __typename = ''> = T extends {[key in string]?: infer U;} ?
    U : T extends (infer R)[] ?
        R: never;

type GetHogeQuery = {
  __typename?: 'Query';
  hoge: {
    __typename?: 'Hoge';
    piyo: Array<{
        __typename?: 'Piyo';
        fuga: Array<{
            __typename?: 'Fuga';
            mofu: string;
        }>
    }>
  }
};

type Q1 = extractTypeName<GetHogeQuery>;
//👇
// type Q1 = "Query" | {
//     __typename?: "Hoge";
//     piyo: Array<{
//         __typename?: "Piyo";
//         fuga: Array<{
//             __typename?: "Fuga";
//             mofu: string;
//         }>;
//     }>;
// }

type Q2 = extractTypeName<Q1>;
//👇
// type Q2 = "Hoge" | {
//     __typename?: "Piyo";
//     fuga: Array<{
//         __typename?: "Fuga";
//         mofu: string;
//     }>;
// }[]

type Q3 = extractTypeName<Q2>;
//👇
// type Q3 = {
//     __typename?: "Piyo";
//     fuga: Array<{
//         __typename?: "Fuga";
//         mofu: string;
//     }>;
// }

type Q4 = extractTypeName<Q3>;
//👇
// type Q4 = "Piyo" | {
//     __typename?: "Fuga";
//     mofu: string;
// }[]

type Q5 = extractTypeName<Q4>;
//👇
// type Q5 = {
//     __typename?: "Fuga";
//     mofu: string;
// }

type Q6 = extractTypeName<Q5>;
//👇
// type Q6 = string
        
このextractTypeName型は、Mapped Typesを使って、オブジェクト型の場合にはオブジェクトのプロパティの型のUnion型を返し、配列型の部分型の場合にはその配列型の要素の型を返します。

extractTypeName型の中で、型を評価する際に、Conditional Typesを2回使って評価していますが、これは以下のようにまとめることができます。

            
            type extractTypeName<T, __typename = ''> = T extends |
    {[key in string]?: infer U;} | (infer U)[] ?
        U: never;
        
こちらのほうが、同じ型変数を2回評価するより、断然スッキリと書けます。

次に改めて、型変数
__typenameに文字列を指定して、__typenameの名前が一致するものだけを抽出できるように修正してみましょう。

            
            type extractTypeName<T, __typename = ''> = T extends |
    {[key in string]?: infer U;} | (infer U)[] ?
        U extends { __typename?: __typename }?
            U: never
        :never;

type Q1 = extractTypeName<GetHogeQuery,'Hoge'>;
//👇
// type Q1 = {
//     __typename?: "Hoge";
//     piyo: Array<{
//         __typename?: "Piyo";
//         fuga: Array<{
//             __typename?: "Fuga";
//             mofu: string;
//         }>;
//     }>;
// }

type Q2 = extractTypeName<Q1>;
//👇
//type Q2 = never
        
プロパティの名前が一致するオブジェクト型のみ抽出したかったわけですが、単純な回帰処理だと、配列型の要素のプロパティにはマッチさせられないので、手動による連鎖が続かないようになってしまいました。

ということは、
__typenameプロパティに名前がマッチしない場合には、更に処理を繰り返して、配列型でなく(つまりオブジェクト型に)なるまで再帰的に型を呼び出しすると上手く行きそうです。

            
            type extractTypeName<T, __typename = ''> = T extends |
    {[key in string]?: infer U;} | (infer U)[] ?
        U extends { __typename?: __typename }?
            U: extractTypeName<U, __typename>
        :never;

type Q1 = extractTypeName<GetHogeQuery,'Hoge'>;
//👇
// type Q1 = {
//     __typename?: "Hoge";
//     piyo: Array<{
//         __typename?: "Piyo";
//         fuga: Array<{
//             __typename?: "Fuga";
//             mofu: string;
//         }>;
//     }>;
// }

type Q2 = extractTypeName<GetHogeQuery, 'Piyo'>;
///👇
// type Q2 = {
//     __typename?: "Piyo";
//     fuga: Array<{
//         __typename?: "Fuga";
//         mofu: string;
//     }>;
// }

type Q3 = extractTypeName<GetHogeQuery, 'Fuga'>;
//👇
// type Q3 = {
//     __typename?: "Fuga";
//     mofu: string;
// }
        
これで自動生成されたGraphQLの長ったらしいクエリの型から、確認したいメンバーの型だけをじっくりみることができるようになりました。

めでたしめでたし。


合同会社タコスキングダム|蛸壺の技術ブログJavascript(js)&Typescript(ts)プログラミング入門〜これから学ぶ人のためのおすすめ書籍&教材の手引き

まとめ

今回は、Typescriptのテンプレートリテラル等のテクニックを駆使して、GraphQL Code Generatorから得られた複雑な型定義から、もっと見やすい型定義へ変換する方法を著者流に解説してみました。

BFFにGraphQLを使う人には開発時に便利なテクニックになるのではと考えます。
記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。

合同会社タコスキングダム|蛸壺の技術ブログJavascript(js)&Typescript(ts)プログラミング入門〜これから学ぶ人のためのおすすめ書籍&教材の手引き